20260408:1、数据上报物模型公式计算bug修复;2、部门/公司支持展示自有logo、公司名称;3、只要设备存在上报imei等数据即刻保存

This commit is contained in:
zhugongping 2026-04-08 17:51:47 +08:00
parent 4048df2cca
commit fd04f3c74c
15 changed files with 272 additions and 79 deletions

View File

@ -118,6 +118,11 @@ public class SysDept extends BaseEntity {
* 机构类型 * 机构类型
*/ */
private Integer deptType; private Integer deptType;
/**
* 机构Logo URL
*/
private String logo;
/** /**
* 是否显示自己 * 是否显示自己
*/ */
@ -151,6 +156,14 @@ public class SysDept extends BaseEntity {
this.deptType = deptType; this.deptType = deptType;
} }
public String getLogo() {
return logo;
}
public void setLogo(String logo) {
this.logo = logo;
}
public String getUserName() { public String getUserName() {
return userName; return userName;
} }

View File

@ -15,7 +15,7 @@ import java.util.regex.Pattern;
/** /**
* 字符串公式计算工具 * 字符串公式计算工具
*/ */
public class CaculateUtils { public class CalculateUtils {
/** /**
* /* * /*
@ -26,14 +26,15 @@ public class CaculateUtils {
/** /**
* 公式计算 字符串 * 公式计算 字符串
* *
* @param exeStr * @param exeStr 公式:例:{AngY}-(0.0047*({T}-20))
* @param replaceMap K:identifier,V:值
*/ */
public static BigDecimal execute(String exeStr, Map<String, String> replaceMap) { public static BigDecimal execute(String exeStr, Map<String, String> replaceMap) {
// 替换掉占位符 // 替换掉占位符
exeStr = caculateReplace(exeStr, replaceMap); exeStr = calculateReplace(exeStr, replaceMap);
exeStr = exeStr.replaceAll("\\s*", ""); exeStr = exeStr.replaceAll("\\s*", "");
List<String> suffixList = suffixHandle(exeStr); List<String> suffixList = suffixHandle(exeStr);
return caculateAnalyse(suffixList); return calculateAnalyse(suffixList);
} }
@ -41,26 +42,25 @@ public class CaculateUtils {
* 公式计算 后序list * 公式计算 后序list
* *
* @param suffixList * @param suffixList
* @return * @return 计算结果
*/ */
public static BigDecimal caculateAnalyse(List<String> suffixList) { public static BigDecimal calculateAnalyse(List<String> suffixList) {
BigDecimal a = BigDecimal.ZERO; BigDecimal a = BigDecimal.ZERO;
BigDecimal b = BigDecimal.ZERO; BigDecimal b;
// 构建一个操作数栈 每当获得操作符号时取出最上面两个数进行计算。 // 构建一个操作数栈 每当获得操作符号时取出最上面两个数进行计算。
Deque<BigDecimal> caculateStack = new ArrayDeque<>(); Deque<BigDecimal> calculateStack = new ArrayDeque<>();
if (suffixList.size() > 1) { if (suffixList.size() > 1) {
for (int i = 0; i < suffixList.size(); i++) { for (String temp : suffixList) {
String temp = suffixList.get(i);
if (symbol.contains(temp)) { if (symbol.contains(temp)) {
b = caculateStack.pop(); b = calculateStack.pop();
a = caculateStack.pop(); a = calculateStack.pop();
a = caculate(a, b, temp.toCharArray()[0]); a = calculate(a, b, temp.toCharArray()[0]);
caculateStack.push(a); calculateStack.push(a);
} else { } else {
if (isNumber(suffixList.get(i))) { if (isNumber(temp)) {
caculateStack.push(new BigDecimal(suffixList.get(i))); calculateStack.push(new BigDecimal(temp));
} else { } else {
throw new RuntimeException("公式异常!"); throw new RuntimeException("公式异常!");
} }
@ -85,7 +85,7 @@ public class CaculateUtils {
* @param symbol * @param symbol
* @return * @return
*/ */
public static BigDecimal caculate(BigDecimal a, BigDecimal b, char symbol) { public static BigDecimal calculate(BigDecimal a, BigDecimal b, char symbol) {
switch (symbol) { switch (symbol) {
case '+': { case '+': {
return a.add(b).stripTrailingZeros(); return a.add(b).stripTrailingZeros();
@ -158,9 +158,9 @@ public class CaculateUtils {
char con = stack.peek(); char con = stack.peek();
if (con == '(') { if (con == '(') {
stack.pop(); stack.pop();
} else { break; // 只匹配当前这一对括号
res.add(String.valueOf(stack.pop()));
} }
res.add(String.valueOf(stack.pop()));
} }
continue; continue;
} }
@ -208,18 +208,18 @@ public class CaculateUtils {
} }
/** /**
* 判断是否为数 字符串 * 判断是否为数字
* *
* @param str * @param str 字符串
* @return * @return true:数字;false:非数字
*/ */
public static boolean isNumber(String str) { public static boolean isNumber(String str) {
Pattern pattern = Pattern.compile("[-+]?[0-9]+\\.{0,1}[0-9]*"); Pattern pattern = Pattern.compile("[-+]?[0-9]+\\.?[0-9]*");
Matcher isNum = pattern.matcher(str); Matcher isNum = pattern.matcher(str);
return isNum.matches(); return isNum.matches();
} }
public static String caculateReplace(String str, Map<String, String> map) { public static String calculateReplace(String str, Map<String, String> map) {
for (Map.Entry<String, String> entry : map.entrySet()) { for (Map.Entry<String, String> entry : map.entrySet()) {
// 对于包含特殊字符的占位符(如 {identifier}),需要转义正则表达式特殊字符 // 对于包含特殊字符的占位符(如 {identifier}),需要转义正则表达式特殊字符
String key = entry.getKey(); String key = entry.getKey();
@ -343,26 +343,26 @@ public class CaculateUtils {
ModbusDataType type = ModbusDataType.convert(dataType); ModbusDataType type = ModbusDataType.convert(dataType);
switch (type) { switch (type) {
case U_SHORT: case U_SHORT:
value = CaculateUtils.toUnSign16(val); value = CalculateUtils.toUnSign16(val);
break; break;
case SHORT: case SHORT:
case LONG_ABCD: case LONG_ABCD:
value = val + ""; value = val + "";
break; break;
case LONG_CDAB: case LONG_CDAB:
value = CaculateUtils.toSign32_CDAB(val); value = CalculateUtils.toSign32_CDAB(val);
break; break;
case U_LONG_ABCD: case U_LONG_ABCD:
value = CaculateUtils.toUnSign32_ABCD(val); value = CalculateUtils.toUnSign32_ABCD(val);
break; break;
case U_LONG_CDAB: case U_LONG_CDAB:
value = CaculateUtils.toUnSign32_CDAB(val); value = CalculateUtils.toUnSign32_CDAB(val);
break; break;
case FLOAT_ABCD: case FLOAT_ABCD:
value = CaculateUtils.toFloat32_ABCD(bytes) + ""; value = CalculateUtils.toFloat32_ABCD(bytes) + "";
break; break;
case FLOAT_CDAB: // todo:待改枚举页面枚举 case FLOAT_CDAB: // todo:待改枚举页面枚举
value = CaculateUtils.toFloat32_DCBA(bytes) + ""; value = CalculateUtils.toFloat32_DCBA(bytes) + "";
break; break;
} }
} }
@ -401,16 +401,17 @@ public class CaculateUtils {
// replaceMap.put("B", "2.5"); // replaceMap.put("B", "2.5");
// replaceMap.put("C", "3.5"); // replaceMap.put("C", "3.5");
// replaceMap.put("D", "4.5"); // replaceMap.put("D", "4.5");
replaceMap.put("AngZ", "-10"); replaceMap.put("{AngY}", "-0.10590162");
// replaceMap.put("%s", "1"); replaceMap.put("{T}", "21.656084");
// replaceMap.put("%s1", "5"); // replaceMap.put("%s1", "5");
BigDecimal execute = execute("AngZ+88.59", replaceMap); String formula = "{AngY}-(0.0047*({T}-20))";
BigDecimal execute = execute(formula, replaceMap);
System.out.println(execute); System.out.println(execute);
Map<String, String> map = new HashMap<>(); /* Map<String, String> map = new HashMap<>();
map.put("%s", "10"); map.put("%s", "10");
String caculate = caculateReplace("%s*2", map); String calculate = calculateReplace("%s*2", map);
System.out.println(caculate); System.out.println(calculate);
System.out.println(execute("%s%3.00", map)); System.out.println(execute("%s%3.00", map));
String s4 = toUnSign16(-1); String s4 = toUnSign16(-1);
@ -451,7 +452,7 @@ public class CaculateUtils {
int val1 = -6553510; int val1 = -6553510;
byte[] bytes2 = intToBytes2(val1); byte[] bytes2 = intToBytes2(val1);
int i1 = bytesToInt2(bytes2); int i1 = bytesToInt2(bytes2);
System.out.println(i1); System.out.println(i1);*/
} }
} }

View File

@ -1,7 +1,7 @@
package com.fastbee.common.utils.gateway; package com.fastbee.common.utils.gateway;
import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.gateway.protocol.ByteUtils; import com.fastbee.common.utils.gateway.protocol.ByteUtils;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
@ -141,7 +141,7 @@ public class CRC16Utils {
byte[] byte8 = ByteUtils.hexToByte(crc8); byte[] byte8 = ByteUtils.hexToByte(crc8);
int b = CRC8(byte8); int b = CRC8(byte8);
System.out.println(b); System.out.println(b);
float v = CaculateUtils.toFloat32_DCBA(bytes); float v = CalculateUtils.toFloat32_DCBA(bytes);
System.out.println(v); System.out.println(v);
} }

View File

@ -314,25 +314,25 @@ public class DeviceDebugLogAspect {
} }
// 更新IMEI、ICCID、longitude、latitude // 更新IMEI、ICCID、longitude、latitude
if (StringUtils.isEmpty(device.getImei()) || StringUtils.isEmpty(device.getIccid())) {
Map<String, String> atCommandMap = ATCommandParserUtils.parseATResponseFromHex(context.sourceData); Map<String, String> atCommandMap = ATCommandParserUtils.parseATResponseFromHex(context.sourceData);
if (!atCommandMap.isEmpty()) {
Device updateDevice = new Device(); Device updateDevice = new Device();
if (StringUtils.isEmpty(device.getImei())) { String imei = atCommandMap.get("IMEI");
updateDevice.setImei(atCommandMap.get("IMEI")); if (StringUtils.isNotEmpty(imei)) {
updateDevice.setImei(imei);
} }
if (StringUtils.isEmpty(device.getIccid())) { String iccid = atCommandMap.get("ICCID");
updateDevice.setIccid(atCommandMap.get("ICCID")); if (StringUtils.isNotEmpty(iccid)) {
updateDevice.setIccid(iccid);
} }
if (device.getLongitude() == null && device.getLatitude() == null) {
String lbsValue = atCommandMap.get("LBS"); String lbsValue = atCommandMap.get("LBS");
if (lbsValue != null) { if (StringUtils.isNotEmpty(lbsValue)) {
String[] lbsArray = lbsValue.split(","); String[] lbsArray = lbsValue.split(",");
if (lbsArray.length == 2) { if (lbsArray.length == 2) {
updateDevice.setLongitude(new BigDecimal(lbsArray[1])); updateDevice.setLongitude(new BigDecimal(lbsArray[1]));
updateDevice.setLatitude(new BigDecimal(lbsArray[0])); updateDevice.setLatitude(new BigDecimal(lbsArray[0]));
} }
} }
}
updateDevice.setDeviceId(device.getDeviceId()); updateDevice.setDeviceId(device.getDeviceId());
updateDevice.setUpdateBy("DTU"); updateDevice.setUpdateBy("DTU");
updateDevice.setUpdateTime(DateUtils.getNowDate()); updateDevice.setUpdateTime(DateUtils.getNowDate());

View File

@ -8,7 +8,7 @@ import com.fastbee.common.core.notify.config.WeChatConfigParams;
import com.fastbee.common.core.notify.msg.WechatMsgParams; import com.fastbee.common.core.notify.msg.WechatMsgParams;
import com.fastbee.common.core.redis.RedisKeyBuilder; import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.DataLevelEnum; import com.fastbee.common.enums.DataLevelEnum;
import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.wechat.WechatUtils; import com.fastbee.common.utils.wechat.WechatUtils;
import com.fastbee.common.wechat.WeChatAppResult; import com.fastbee.common.wechat.WeChatAppResult;
@ -317,7 +317,7 @@ public class ThingsModelPushHandler {
replaceMap.put("{" + refKey + "}", val); replaceMap.put("{" + refKey + "}", val);
} }
try { try {
Object computed = CaculateUtils.execute(model.getFormula(), replaceMap); Object computed = CalculateUtils.execute(model.getFormula(), replaceMap);
String computedStr = computed != null ? computed.toString() : null; String computedStr = computed != null ? computed.toString() : null;
return new ComputedValue(computedStr, maxTs); return new ComputedValue(computedStr, maxTs);
} catch (Exception e) { } catch (Exception e) {

View File

@ -13,7 +13,7 @@ import com.fastbee.common.core.protocol.modbus.ModbusCode;
import com.fastbee.common.core.redis.RedisCache; import com.fastbee.common.core.redis.RedisCache;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem; import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.common.enums.FunctionReplyStatus; import com.fastbee.common.enums.FunctionReplyStatus;
import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.gateway.CRC16Utils; import com.fastbee.common.utils.gateway.CRC16Utils;
@ -229,7 +229,7 @@ public class ModbusProtocol implements IProtocol {
//普通取值,应该只有一个数据,将identity与address替换 //普通取值,应该只有一个数据,将identity与address替换
ThingsModelSimpleItem simpleItem = new ThingsModelSimpleItem(); ThingsModelSimpleItem simpleItem = new ThingsModelSimpleItem();
simpleItem.setId(config.getIdentifier()); simpleItem.setId(config.getIdentifier());
simpleItem.setValue(CaculateUtils.handleToUnSign16(data[0] + "", config.getDataType())); simpleItem.setValue(CalculateUtils.handleToUnSign16(data[0] + "", config.getDataType()));
simpleItem.setTs(DateUtils.getNowDate()); simpleItem.setTs(DateUtils.getNowDate());
simpleItemList.add(simpleItem); simpleItemList.add(simpleItem);
} }
@ -301,11 +301,11 @@ public class ModbusProtocol implements IProtocol {
if (quantity == 1) { if (quantity == 1) {
// 单个寄存器 // 单个寄存器
String value = CaculateUtils.handleToUnSign16(Long.parseLong(handleHex, 16) + "", modbusConfig.getDataType()); String value = CalculateUtils.handleToUnSign16(Long.parseLong(handleHex, 16) + "", modbusConfig.getDataType());
simpleItem.setValue(value); simpleItem.setValue(value);
} else if (quantity == 2) { } else if (quantity == 2) {
// 浮点数,2个寄存器 // 浮点数,2个寄存器
String value = CaculateUtils.parseValue(modbusConfig.getDataType(), handleHex); String value = CalculateUtils.parseValue(modbusConfig.getDataType(), handleHex);
simpleItem.setValue(value); simpleItem.setValue(value);
} else { } else {
log.warn("不支持的quantity值:{},配置地址:{}", quantity, configAddress); log.warn("不支持的quantity值:{},配置地址:{}", quantity, configAddress);

View File

@ -2,7 +2,7 @@ package com.fastbee.modbus.test;
import com.fastbee.common.core.mq.message.DeviceData; import com.fastbee.common.core.mq.message.DeviceData;
import com.fastbee.common.core.protocol.modbus.ModbusCode; import com.fastbee.common.core.protocol.modbus.ModbusCode;
import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.gateway.protocol.ByteUtils; import com.fastbee.common.utils.gateway.protocol.ByteUtils;
import com.fastbee.modbus.codec.ModbusDecoder; import com.fastbee.modbus.codec.ModbusDecoder;
import com.fastbee.modbus.codec.ModbusEncoder; import com.fastbee.modbus.codec.ModbusEncoder;
@ -78,7 +78,7 @@ public class ModbusTest {
System.out.println(s1); System.out.println(s1);
String hexToBytes = "41EE8000"; String hexToBytes = "41EE8000";
byte[] bytes = ByteBufUtil.decodeHexDump(hexToBytes); byte[] bytes = ByteBufUtil.decodeHexDump(hexToBytes);
String v = CaculateUtils.toFloat(bytes); String v = CalculateUtils.toFloat(bytes);
System.out.println(v); System.out.println(v);
System.out.println(ByteUtils.intToHex(-32768)); System.out.println(ByteUtils.intToHex(-32768));

View File

@ -15,7 +15,7 @@ import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem; import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.common.enums.FunctionReplyStatus; import com.fastbee.common.enums.FunctionReplyStatus;
import com.fastbee.common.exception.ServiceException; import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.modbus.BitUtils; import com.fastbee.common.utils.modbus.BitUtils;
@ -265,7 +265,7 @@ public class ModbusTcpProtocol implements IProtocol {
//普通取值,应该只有一个数据,将identity与address替换 //普通取值,应该只有一个数据,将identity与address替换
ThingsModelSimpleItem simpleItem = new ThingsModelSimpleItem(); ThingsModelSimpleItem simpleItem = new ThingsModelSimpleItem();
simpleItem.setId(config.getIdentifier()); simpleItem.setId(config.getIdentifier());
simpleItem.setValue(CaculateUtils.handleToUnSign16(data[0] + "", config.getDataType())); simpleItem.setValue(CalculateUtils.handleToUnSign16(data[0] + "", config.getDataType()));
simpleItem.setTs(DateUtils.getNowDate()); simpleItem.setTs(DateUtils.getNowDate());
simpleItemList.add(simpleItem); simpleItemList.add(simpleItem);
} }
@ -306,13 +306,13 @@ public class ModbusTcpProtocol implements IProtocol {
simpleItem.setTs(DateUtils.getNowDate()); simpleItem.setTs(DateUtils.getNowDate());
if (quantity == 1) { if (quantity == 1) {
//将identity与address替换 //将identity与address替换
simpleItem.setValue(CaculateUtils.handleToUnSign16(data[i] + "", modbusConfig.getDataType())); simpleItem.setValue(CalculateUtils.handleToUnSign16(data[i] + "", modbusConfig.getDataType()));
simpleItemList.add(simpleItem); simpleItemList.add(simpleItem);
dataHex = dataHex.substring(4); dataHex = dataHex.substring(4);
} else if (quantity == 2) { } else if (quantity == 2) {
//这里做数据处理 //这里做数据处理
String handleHex = dataHex.substring(0, 8); String handleHex = dataHex.substring(0, 8);
String value = CaculateUtils.parseValue(modbusConfig.getDataType(), handleHex); String value = CalculateUtils.parseValue(modbusConfig.getDataType(), handleHex);
simpleItem.setValue(value); simpleItem.setValue(value);
simpleItemList.add(simpleItem); simpleItemList.add(simpleItem);
//处理dataHex //处理dataHex

View File

@ -5,7 +5,7 @@ import com.fastbee.common.enums.scenemodel.SceneModelVariableTypeEnum;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.SecurityUtils; import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.iot.cache.SceneModelTagCache; import com.fastbee.iot.cache.SceneModelTagCache;
import com.fastbee.iot.domain.*; import com.fastbee.iot.domain.*;
import com.fastbee.iot.model.AlertCountVO; import com.fastbee.iot.model.AlertCountVO;
@ -380,7 +380,7 @@ public class DataCenterServiceImpl implements DataCenterService {
replaceMap.put("{" + entry.getKey() + "}", val != null ? val : "0"); replaceMap.put("{" + entry.getKey() + "}", val != null ? val : "0");
} }
try { try {
String computed = CaculateUtils.execute(model.getFormula(), replaceMap).toString(); String computed = CalculateUtils.execute(model.getFormula(), replaceMap).toString();
HistoryModel hm = new HistoryModel(); HistoryModel hm = new HistoryModel();
hm.setTime(minute); hm.setTime(minute);
hm.setIdentity(calcIdentifier); hm.setIdentity(calcIdentifier);

View File

@ -19,7 +19,7 @@ import com.fastbee.common.core.thingsModel.ThingsModelSimpleItem;
import com.fastbee.common.core.thingsModel.ThingsModelValuesInput; import com.fastbee.common.core.thingsModel.ThingsModelValuesInput;
import com.fastbee.common.enums.*; import com.fastbee.common.enums.*;
import com.fastbee.common.exception.ServiceException; import com.fastbee.common.exception.ServiceException;
import com.fastbee.common.utils.CaculateUtils; import com.fastbee.common.utils.CalculateUtils;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.SecurityUtils; import com.fastbee.common.utils.SecurityUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
@ -583,7 +583,7 @@ public class DeviceServiceImpl implements IDeviceService {
// 执行计算 // 执行计算
try { try {
value = String.valueOf(CaculateUtils.execute(formula, params)); value = String.valueOf(CalculateUtils.execute(formula, params));
} catch (Exception e) { } catch (Exception e) {
log.error("=>计算公式执行失败[{}],标识符:[{}],公式:[{}],错误:{}", input.getProductId(), identity, formula, e.getMessage()); log.error("=>计算公式执行失败[{}],标识符:[{}],公式:[{}],错误:{}", input.getProductId(), identity, formula, e.getMessage());
} }

View File

@ -22,10 +22,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" /> <result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" /> <result property="updateTime" column="update_time" />
<result property="deptType" column="dept_type" /> <result property="deptType" column="dept_type" />
<result property="logo" column="logo" />
</resultMap> </resultMap>
<sql id="selectDeptVo"> <sql id="selectDeptVo">
select d.dept_id, d.dept_user_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time, d.dept_type select d.dept_id, d.dept_user_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.del_flag, d.create_by, d.create_time, d.dept_type, d.logo
from sys_dept d from sys_dept d
</sql> </sql>
@ -61,7 +62,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult"> <select id="selectDeptById" parameterType="Long" resultMap="SysDeptResult">
select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.dept_type, d.dept_user_id, select d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.phone, d.email, d.status, d.dept_type, d.dept_user_id, d.logo,
(select dept_name from sys_dept where dept_id = d.parent_id) parent_name (select dept_name from sys_dept where dept_id = d.parent_id) parent_name
from sys_dept d from sys_dept d
where d.dept_id = #{deptId} where d.dept_id = #{deptId}
@ -135,6 +136,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="status != null">status,</if> <if test="status != null">status,</if>
<if test="createBy != null and createBy != ''">create_by,</if> <if test="createBy != null and createBy != ''">create_by,</if>
<if test="deptType != null and deptType != ''">dept_type,</if> <if test="deptType != null and deptType != ''">dept_type,</if>
<if test="logo != null and logo != ''">logo,</if>
create_time create_time
)values( )values(
<if test="deptId != null and deptId != 0">#{deptId},</if> <if test="deptId != null and deptId != 0">#{deptId},</if>
@ -149,6 +151,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="status != null">#{status},</if> <if test="status != null">#{status},</if>
<if test="createBy != null and createBy != ''">#{createBy},</if> <if test="createBy != null and createBy != ''">#{createBy},</if>
<if test="deptType != null and deptType != ''">#{deptType},</if> <if test="deptType != null and deptType != ''">#{deptType},</if>
<if test="logo != null and logo != ''">#{logo},</if>
sysdate() sysdate()
) )
</insert> </insert>
@ -166,6 +169,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="email != null">email = #{email},</if> <if test="email != null">email = #{email},</if>
<if test="status != null and status != ''">status = #{status},</if> <if test="status != null and status != ''">status = #{status},</if>
<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if> <if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
<if test="logo != null">logo = #{logo},</if>
update_time = sysdate() update_time = sysdate()
</set> </set>
where dept_id = #{deptId} where dept_id = #{deptId}

View File

@ -38,6 +38,7 @@
<result property="leader" column="leader" /> <result property="leader" column="leader" />
<result property="status" column="dept_status" /> <result property="status" column="dept_status" />
<result property="deptUserId" column="dept_user_id"/> <result property="deptUserId" column="dept_user_id"/>
<result property="logo" column="logo" />
<result property="deptUserName" column="dept_user_name"/> <result property="deptUserName" column="dept_user_name"/>
</resultMap> </resultMap>
@ -52,7 +53,7 @@
<sql id="selectUserVo"> <sql id="selectUserVo">
select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, d.dept_user_id, d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, d.dept_user_id,d.logo,
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status, r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,
(select su.user_name from sys_user su where su.user_id = d.dept_user_id) as dept_user_name, (select su.user_name from sys_user su where su.user_id = d.dept_user_id) as dept_user_name,
ap.language,ap.time_zone ap.language,ap.time_zone

View File

@ -0,0 +1,165 @@
# 数据中心 · 历史记录 · 曲线趋势图改造方案
## 1. 背景与问题
### 1.1 业务场景
路径:**数据中心 → 历史记录**([`history.vue`](../src/views/dataCenter/history.vue))。
页面含三个 Tab:**设备**、**网关设备**、**场景**,各自有「曲线趋势图」或网关下的计算量 / 多设备对比 / 单子设备折线图。
### 1.2 原始痛点
| 问题 | 说明 |
|------|------|
| 多曲线「压成直线」 | 多物模型量级差异大(如位移 168、角度 3、电压 12),共用单 Y 轴时范围被拉大,小幅波动在图上不可见。 |
| 多 Y 轴布局异常 | 为每条曲线建隐藏 Y 轴时,ECharts 会为多轴计算偏移,曲线一多时绘图区被挤到一侧。 |
| 图例占用空间 | 网关多设备对比图例项极多,默认换行占满高度,挤压 grid。 |
| X 轴可读性 | 时间点密集时标签重叠。 |
| 垂直空间不足 | 固定 480px 高度时,几十条归一化曲线仍显拥挤。 |
---
## 2. 总体思路
1. **独立 Y 轴模式**:不再使用「每 series 一个 Y 轴」,改为 **按每条曲线自身 min-max 归一化到 [-100, 100]**,**共用一条数值 Y 轴**,避免多轴布局副作用;**Tooltip 始终展示原始值**。
2. **共享 Y 轴模式**:保留原逻辑,单 Y 轴 + `scale: true`,便于对比绝对量级。
3. **归一化坐标展示**:独立模式下 Y 轴可见,刻度约 -100%~100%,并附说明性网格线。
4. **网关图**:图例与 grid 与设备 Tab 对齐策略;X 轴旋转 + 对齐刻度;必要时配合底部留白与 dataZoom。
5. **动态高度**:按 **series 数量** 增加图表容器高度(有上下限),并在 DOM 更新后 `resize`。
---
## 3. 功能开关
- **数据字段**:`independentYAxis`(默认 `true`)。
- **UI**:设备 / 网关 / 场景三个图表卡片头部 **「独立Y轴 | 共享Y轴」** 开关。
- **切换行为**:`handleYAxisModeChange` 在 `$nextTick` 内按当前 Tab 与已有数据调用对应 `draw*Line`,无需重新请求接口。
---
## 4. 核心实现
### 4.1 归一化(独立模式)
**方法**:`applyIndependentScale(series)`
- 对每条 `series.data` 中可解析为数字的点,取该 series 全体有效点的 `min`、`max`。
- 映射公式:`value = ((num - min) / (max - min)) * 200 - 100`,落在 **[-100, 100]**。
- **常量序列**(`max === min`):映射为 **0**(中间水平线)。
- 每个数据点存为 `{ value, _original }`,`_original` 为归一化前的原始值(兼容负数与非数字展示)。
**返回的 Y 轴配置**(单轴):
- `min: -100`, `max: 100`
- `axisLine` / `axisTick` 显示
- `axisLabel.formatter`: 显示为 `xx%`(表示相对本曲线量程的位置,非物理单位)
- `splitLine`:虚线网格,便于读趋势
### 4.2 Tooltip
**方法**:`independentScaleTooltip()`
- `trigger: 'axis'`
- `formatter`:时间轴标签 + 各 series **原始值**(从 `p.data._original` 读取)
### 4.3 共享模式
- `yAxis`: `{ type: 'value', scale: true }`
- `tooltip`: `{ trigger: 'axis' }`(默认)
### 4.4 涉及的绘图方法
| 方法 | Tab / 场景 |
|------|------------|
| `drawDevLine` | 设备 |
| `drawGwCalcLine` | 网关 · 计算量 |
| `drawGwSingleLine` | 网关 · 单子设备 |
| `drawSubCompareLine` | 网关 · 多设备同一变量对比 |
| `drawSceneLine` | 场景 |
以上方法在 `independentYAxis === true` 时均走 `applyIndependentScale` + `independentScaleTooltip`。
### 4.5 网关图布局要点
- `legend`: `align: 'right'`, `left: '3%'`, `top: '3%'`(与多行图例协调)
- `grid.top: '30%'`,为图例留出区域,避免与曲线重叠
- `xAxis`: `boundaryGap: true`, `axisTick.alignWithLabel: true`, `axisLabel.rotate: 30`
- `dataZoom`: `inside` + 滑块,便于时间范围缩放
- `toolbox`: 数据视图、还原、保存图片(与设备 Tab 能力对齐)
### 4.6 动态图表高度
**数据字段**(像素):
- `devChartHeight`, `gwCalcChartHeight`, `gwSingleChartHeight`, `subCompareChartHeight`, `sceneChartHeight`(默认 480)
**方法**:`calcChartHeight(seriesCount)`
```text
height = clamp(480, seriesCount * 14 + 360, 1200)
```
即:最少 480px,随曲线数量增加,**最多 1200px**。
**模板**:各图表容器使用 `:style="{ height: xxxChartHeight + 'px', ... }"`。
**绘制流程**:
1. 先算好 `series` 与 `series.length`,赋值对应 `xxxChartHeight`
2. `this.$nextTick(() => { init/clear → setOption → chart.resize() })`
保证先根据新高度更新 DOM,再初始化/重绘 ECharts。
---
## 5. 数据流(简图)
```mermaid
flowchart LR
subgraph input [数据]
API[历史接口]
end
subgraph mode [模式]
IND[独立Y轴]
SHR[共享Y轴]
end
subgraph render [渲染]
NORM[逐条 min-max 到 -100~100]
ONEAXIS[单 Y 轴]
TT[Tooltip 原始值]
end
API --> IND
API --> SHR
IND --> NORM --> ONEAXIS
ONEAXIS --> TT
SHR --> ONEAXIS
```
---
## 6. 使用说明(给产品/测试)
1. **独立 Y 轴(推荐多曲线看趋势)**:纵轴为各曲线**自身量程内的相对位置**(-100%~100%),对比不同物理量时看形状;**真实数值以 Tooltip 与下方表格为准**。
2. **共享 Y 轴**:纵轴为**真实数值**,适合量级接近、需比大小的变量;量级差很大时仍可能出现「看起来像直线」。
3. **曲线很多时**:图会自动变高(上限约 1200px),页面可纵向滚动查看全图。
---
## 7. 相关文件
| 文件 | 说明 |
|------|------|
| [`yack-web/src/views/dataCenter/history.vue`](../src/views/dataCenter/history.vue) | 全部图表逻辑、开关、高度、归一化 |
| [`yack-web/src/api/data/center.js`](../src/api/data/center.js) | 历史数据接口(未因本方案变更协议) |
---
## 8. 版本与维护建议
- 若需调整「多高算够」,改 `calcChartHeight` 中的系数 `14`、基础 `360`、上下限 `480`/`1200` 即可。
- 若产品要求 Y 轴显示「归一化」文案而非 `%`,可在 `applyIndependentScale` 返回的 `axisLabel` 中改为固定文案或 `name`/`nameLocation`。
- 新增同类折线图时,复用:`applyIndependentScale` / `independentScaleTooltip` / `calcChartHeight` + `$nextTick` + `resize` 模式。
---
*文档对应实现:历史记录页曲线趋势图(多 Tab、独立/共享 Y 轴、归一化 -100~100、动态高度)。*

View File

@ -32,12 +32,16 @@ export default {
sideTheme() { sideTheme() {
return this.$store.state.settings.sideTheme; return this.$store.state.settings.sideTheme;
}, },
dept() {
return this.$store.state.user.dept;
},
logo() {
const deptLogo = this.dept && this.dept.logo;
return deptLogo ? process.env.VUE_APP_BASE_API + deptLogo : logoImg;
},
title() {
return (this.dept && this.dept.deptName) || '岩安测控';
}, },
data() {
return {
title: '岩安测控',
logo: logoImg,
};
}, },
}; };
</script> </script>

View File

@ -74,6 +74,9 @@
<el-form-item :label="$t('system.dept.780956-0')" prop="deptName"> <el-form-item :label="$t('system.dept.780956-0')" prop="deptName">
<el-input v-model="form.deptName" :placeholder="$t('system.dept.780956-1')" /> <el-input v-model="form.deptName" :placeholder="$t('system.dept.780956-1')" />
</el-form-item> </el-form-item>
<el-form-item label="机构Logo" prop="logo">
<imageUpload :value="form.logo" :limit="1" :fileSize="2" @input="form.logo = $event" />
</el-form-item>
<el-form-item :label="$t('system.dept.780956-3')" prop="deptType"> <el-form-item :label="$t('system.dept.780956-3')" prop="deptType">
<el-select v-model="form.deptType" :placeholder="$t('system.dept.780956-12')" clearable size="small" style="width: 100%" :disabled="this.form.deptId != null"> <el-select v-model="form.deptType" :placeholder="$t('system.dept.780956-12')" clearable size="small" style="width: 100%" :disabled="this.form.deptId != null">
<el-option v-for="option in deptTypeList" :key="option.value" :label="option.label" :value="option.value"></el-option> <el-option v-for="option in deptTypeList" :key="option.value" :label="option.label" :value="option.value"></el-option>
@ -126,11 +129,12 @@
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild, getDeptType } from '@/api/system/dept'; import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild, getDeptType } from '@/api/system/dept';
import Treeselect from '@riophae/vue-treeselect'; import Treeselect from '@riophae/vue-treeselect';
import '@riophae/vue-treeselect/dist/vue-treeselect.css'; import '@riophae/vue-treeselect/dist/vue-treeselect.css';
import imageUpload from '@/components/ImageUpload/index';
export default { export default {
name: 'Dept', name: 'Dept',
dicts: ['sys_normal_disable', 'department_type'], dicts: ['sys_normal_disable', 'department_type'],
components: { Treeselect }, components: { Treeselect, imageUpload },
data() { data() {
const equalToPassword = (rule, value, callback) => { const equalToPassword = (rule, value, callback) => {
if (this.form.password !== value) { if (this.form.password !== value) {
@ -251,6 +255,7 @@ export default {
status: '0', status: '0',
deptType: 0, deptType: 0,
showOwner: false, showOwner: false,
logo: undefined,
}; };
this.resetForm('form'); this.resetForm('form');
}, },