fix(M): 修复品类中文乱码, 子设备在线状态按父网关判定
乱码: 服务器 mysql 客户端在 C locale 下 character_set_client=latin1, 迁移脚本经 Windows type|plink 管道执行时 UTF-8 字节被按 latin1 解读, 中文双重编码。修复: V25/V26 脚本首行加 SET NAMES utf8mb4, deploy-all.bat 的 mysql 调用补 --default-character-set=utf8mb4, 新增 V26 按 code 写回正确中文(幂等)。已验证 product_category.name 的 hex 为正确 UTF-8。 子设备在线: 涂鸦 Mesh/Zigbee 子设备无独立在线通道, SDK 与云端快照对其 online 恒 false, APP 上报后被写成离线。后端新增 resolveEffectiveOnlineStatus —— 上报在线即 online; 子设备上报离线时以父网关为准(父在线则 online); 设备列表输出再做一次父网关聚合, 历史数据立即生效。前端 DeviceInfo 增报 parentDeviceId, 并按品类推断 is_gateway。 APP: 网关下挂设备不再用涂鸦 SDK 快照覆盖后端结论; 列表状态改为在线/离线/状态未知三态; uniapp 端修复状态比大写 ONLINE 导致恒离线、且把状态字符串当 DP JSON 解析的问题。生产验证: JD2BTSA/JD6BTSA 由 offline 变为 online。
This commit is contained in:
parent
903b44c84b
commit
066d374673
@ -51,6 +51,15 @@ public class TuyaDevice {
|
||||
@SerializedName("parentDeviceId")
|
||||
private String parentDeviceId;
|
||||
|
||||
/**
|
||||
* 后端原始状态值(online / offline / unknown)
|
||||
*
|
||||
* <p>子设备(网关下挂)没有独立在线通道,后端会按父网关聚合状态,
|
||||
* 无法用 online/offline 两态表达"未知",因此保留原始值供 UI 区分展示。</p>
|
||||
*/
|
||||
@SerializedName("statusRaw")
|
||||
private String statusRaw;
|
||||
|
||||
/** 是否为网关 */
|
||||
@SerializedName("isGateway")
|
||||
private Integer isGateway;
|
||||
@ -94,6 +103,9 @@ public class TuyaDevice {
|
||||
public String getParentDeviceId() { return parentDeviceId; }
|
||||
public void setParentDeviceId(String parentDeviceId) { this.parentDeviceId = parentDeviceId; }
|
||||
|
||||
public String getStatusRaw() { return statusRaw; }
|
||||
public void setStatusRaw(String statusRaw) { this.statusRaw = statusRaw; }
|
||||
|
||||
public Integer getIsGateway() { return isGateway; }
|
||||
public void setIsGateway(Integer isGateway) { this.isGateway = isGateway; }
|
||||
|
||||
|
||||
@ -581,6 +581,8 @@ public class DevicesFragment extends Fragment {
|
||||
td.setPlatform(bd.getPlatform());
|
||||
td.setParentDeviceId(bd.getParentDeviceId());
|
||||
td.setIsGateway(bd.getIsGateway());
|
||||
// 保留后端原始状态,供列表区分"在线/离线/状态未知"
|
||||
td.setStatusRaw(bd.getStatus());
|
||||
// 后端设备类型作为兜底(涂鸦设备若 SDK 有 category 会覆盖)
|
||||
if (bd.getDeviceType() != null && !bd.getDeviceType().isEmpty()) {
|
||||
td.setCategory(bd.getDeviceType());
|
||||
@ -593,10 +595,20 @@ public class DevicesFragment extends Fragment {
|
||||
// 在线状态以涂鸦 SDK 实时数据为准(后端 status 可能未及时刷新)
|
||||
boolean sdkOnline = com.xhai.iot.service.TuyaSdkService.resolveDeviceOnline(
|
||||
bd.getDeviceId(), devBean, bd.isOnline(), bd.getParentDeviceId());
|
||||
td.setOnline(sdkOnline);
|
||||
if (sdkOnline != bd.isOnline()) {
|
||||
com.xhai.iot.service.TuyaSdkService
|
||||
.reportStatusToBackend(bd.getDeviceId(), sdkOnline, null);
|
||||
// 网关下挂设备(子设备)特殊处理:涂鸦 SDK / 云端快照对 Mesh、Zigbee
|
||||
// 子设备的 online 恒为 false(通电在线也一样),照搬会把明明在线的
|
||||
// 子设备显示成离线;此时以「父网关在线」为权威依据 —— 后端已按父网关
|
||||
// 聚合过状态,这里直接采信,不再用 SDK 快照覆盖。
|
||||
boolean isSubDevice = bd.getParentDeviceId() != null
|
||||
&& !bd.getParentDeviceId().isEmpty();
|
||||
if (isSubDevice) {
|
||||
td.setOnline(bd.isOnline());
|
||||
} else {
|
||||
td.setOnline(sdkOnline);
|
||||
if (sdkOnline != bd.isOnline()) {
|
||||
com.xhai.iot.service.TuyaSdkService
|
||||
.reportStatusToBackend(bd.getDeviceId(), sdkOnline, null);
|
||||
}
|
||||
}
|
||||
if (devBean.getDps() != null) {
|
||||
td.setDps(devBean.getDps());
|
||||
|
||||
@ -145,7 +145,7 @@ public class DeviceRowAdapter extends RecyclerView.Adapter<DeviceRowAdapter.View
|
||||
TuyaDevice device = devices.get(position);
|
||||
boolean online = device.isOnline();
|
||||
holder.tvName.setText(device.getName());
|
||||
holder.tvSub.setText((online ? "在线" : "离线") + " · 涂鸦");
|
||||
holder.tvSub.setText(statusText(device) + " · 涂鸦");
|
||||
|
||||
// 图标
|
||||
String iconUrl = device.getIcon();
|
||||
@ -175,6 +175,26 @@ public class DeviceRowAdapter extends RecyclerView.Adapter<DeviceRowAdapter.View
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在线状态文案。
|
||||
*
|
||||
* <p>后端对网关下挂的子设备会按父网关聚合状态,取不到权威信息时用 unknown
|
||||
* 表达"状态未知",而不是一律兜底成"离线"(曾导致通电在线的子设备显示离线)。</p>
|
||||
*/
|
||||
private String statusText(TuyaDevice device) {
|
||||
String raw = device.getStatusRaw();
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return device.isOnline() ? "在线" : "离线";
|
||||
}
|
||||
if ("online".equalsIgnoreCase(raw)) {
|
||||
return "在线";
|
||||
}
|
||||
if ("offline".equalsIgnoreCase(raw)) {
|
||||
return "离线";
|
||||
}
|
||||
return "状态未知";
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定胶囊开关与状态文字(抽成独立方法)。
|
||||
* 局部刷新时只调用这里,不重载图标,避免列表闪烁
|
||||
|
||||
@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@ -412,8 +413,12 @@ public class TuyaController {
|
||||
update.setPlatform("tuya");
|
||||
update.setDeviceType(tuyaDevice.getCategory() != null
|
||||
? tuyaDevice.getCategory() : existing.getDeviceType());
|
||||
update.setStatus(Boolean.TRUE.equals(tuyaDevice.getOnline())
|
||||
? "online" : "offline");
|
||||
update.setStatus(resolveEffectiveOnlineStatus(existing, tuyaDevice.getOnline()));
|
||||
if (tuyaDevice.getParentDeviceId() != null && !tuyaDevice.getParentDeviceId().isEmpty()) {
|
||||
// 子设备的父子关系由前端从涂鸦 SDK 一并上报
|
||||
update.setParentDeviceId(tuyaDevice.getParentDeviceId());
|
||||
update.setIsGateway(0);
|
||||
}
|
||||
update.setPlatformProductId(tuyaDevice.getProductId() != null
|
||||
? tuyaDevice.getProductId() : existing.getPlatformProductId());
|
||||
update.setIcon(tuyaDevice.getIcon() != null
|
||||
@ -435,8 +440,9 @@ public class TuyaController {
|
||||
localDevice.setPlatform("tuya");
|
||||
localDevice.setDeviceType(tuyaDevice.getCategory() != null
|
||||
? tuyaDevice.getCategory() : "unknown");
|
||||
localDevice.setStatus(Boolean.TRUE.equals(tuyaDevice.getOnline())
|
||||
? "online" : "offline");
|
||||
localDevice.setStatus(resolveEffectiveOnlineStatus(null, tuyaDevice.getOnline()));
|
||||
localDevice.setParentDeviceId(tuyaDevice.getParentDeviceId());
|
||||
localDevice.setIsGateway(resolveIsGateway(tuyaDevice) ? 1 : 0);
|
||||
localDevice.setLocation(req.getRoomId() != null
|
||||
? String.valueOf(req.getRoomId()) : null);
|
||||
// 家庭/房间真实关联(V11: device.home_id/room_id)
|
||||
@ -526,7 +532,7 @@ public class TuyaController {
|
||||
update.setPlatform(existing.getPlatform());
|
||||
}
|
||||
if (req.getOnline() != null) {
|
||||
update.setStatus(req.getOnline() ? "online" : "offline");
|
||||
update.setStatus(resolveEffectiveOnlineStatus(existing, req.getOnline()));
|
||||
}
|
||||
// 更新DP能力(存到capabilities字段的dps子键,便于查询当前状态)
|
||||
if (req.getDps() != null && !req.getDps().isEmpty()) {
|
||||
@ -561,6 +567,58 @@ public class TuyaController {
|
||||
return Result.success(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算涂鸦设备的有效在线状态。
|
||||
*
|
||||
* <p><b>为什么需要它:</b>涂鸦 Mesh/Zigbee 子设备没有独立的在线通道 —— 涂鸦 SDK 与
|
||||
* OpenAPI 快照对它们的 online 恒为 false(实测:通电在线的 JD6BTSA 云端仍为 false)。
|
||||
* APP 把这个 false 上报后,device.status 就被写成 offline,用户看到「明明通电、网关
|
||||
* 在线,子设备却显示离线」。</p>
|
||||
*
|
||||
* <p><b>判定规则:</b></p>
|
||||
* <ul>
|
||||
* <li>上报在线 → online;</li>
|
||||
* <li>子设备(parent_device_id 非空)上报离线 → 以父网关为准:父网关在线则 online,
|
||||
* 否则 offline;</li>
|
||||
* <li>顶层设备上报离线 → offline。</li>
|
||||
* </ul>
|
||||
*/
|
||||
private String resolveEffectiveOnlineStatus(Device device, Boolean reportedOnline) {
|
||||
if (Boolean.TRUE.equals(reportedOnline)) {
|
||||
return "online";
|
||||
}
|
||||
String parentId = device == null ? null : device.getParentDeviceId();
|
||||
if (parentId != null && !parentId.isEmpty()) {
|
||||
Device parent = deviceMapper.selectByDeviceId(parentId);
|
||||
if (parent != null && "online".equalsIgnoreCase(parent.getStatus())) {
|
||||
log.info("子设备在线状态按父网关判定为在线: device={}, parent={}",
|
||||
device.getDeviceId(), parentId);
|
||||
return "online";
|
||||
}
|
||||
}
|
||||
return "offline";
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为网关设备。
|
||||
*
|
||||
* <p>子设备一定有父设备,因此「没有父设备」且品类属于网关类(wg/gateway/gw)即为网关。</p>
|
||||
*/
|
||||
private boolean resolveIsGateway(TuyaBindRequest.DeviceInfo info) {
|
||||
if (info == null) {
|
||||
return false;
|
||||
}
|
||||
if (info.getParentDeviceId() != null && !info.getParentDeviceId().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String category = info.getCategory();
|
||||
if (category == null) {
|
||||
return false;
|
||||
}
|
||||
String c = category.trim().toLowerCase(Locale.ROOT);
|
||||
return "wg".equals(c) || "gateway".equals(c) || "gw".equals(c) || "sub_gateway".equals(c);
|
||||
}
|
||||
|
||||
// ==================== 设备控制 ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -57,5 +57,15 @@ public class TuyaBindRequest {
|
||||
private String productId;
|
||||
@Schema(description = "设备图标URL")
|
||||
private String icon;
|
||||
/**
|
||||
* 父设备(网关)ID
|
||||
*
|
||||
* <p>涂鸦 Mesh/Zigbee 子设备没有独立在线通道,涂鸦 SDK 与云端快照对它们的
|
||||
* online 恒为 false(实测通电在线的子设备也恒 false)。子设备的在线状态只能
|
||||
* 依据「父网关是否在线」判定,因此前端需把涂鸦 SDK 里的父设备关系一并上报,
|
||||
* 后端据此写入 device.parent_device_id。</p>
|
||||
*/
|
||||
@Schema(description = "父设备(网关)ID")
|
||||
private String parentDeviceId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,9 +174,55 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
|
||||
|
||||
Page<Device> result = page(new Page<>(query.getPageNum(), query.getPageSize()), wrapper);
|
||||
fillProductInfo(result.getRecords());
|
||||
resolveSubDeviceStatus(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 子设备在线状态聚合(仅影响返回结果,不写库)。
|
||||
*
|
||||
* <p>涂鸦 Mesh/Zigbee 子设备没有独立的在线通道 —— 涂鸦 SDK 与 OpenAPI 快照对它们的
|
||||
* online 恒为 false,APP 把这个 false 上报后,库里存的就是 offline。用户明明看到
|
||||
* 子设备通电、网关在线,列表里却显示离线。</p>
|
||||
*
|
||||
* <p>这里以「父网关在线」为判据做一次聚合:父网关在线 → 子设备按在线展示;
|
||||
* 父网关不在线且子设备状态未知 → 按离线展示。写入侧同样有对<EFBFBD><EFBFBD>规则
|
||||
* (TuyaController.resolveEffectiveOnlineStatus),这里是展示侧的兜底,
|
||||
* 保证历史数据也能立即显示正确。</p>
|
||||
*/
|
||||
private void resolveSubDeviceStatus(List<Device> devices) {
|
||||
if (devices == null || devices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> parentIds = devices.stream()
|
||||
.map(Device::getParentDeviceId)
|
||||
.filter(StringUtils::hasText)
|
||||
.collect(Collectors.toSet());
|
||||
if (parentIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Device> parentMap = baseMapper.selectList(new LambdaQueryWrapper<Device>()
|
||||
.in(Device::getDeviceId, parentIds))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Device::getDeviceId, d -> d, (a, b) -> a));
|
||||
for (Device device : devices) {
|
||||
String parentId = device.getParentDeviceId();
|
||||
if (!StringUtils.hasText(parentId)) {
|
||||
continue;
|
||||
}
|
||||
Device parent = parentMap.get(parentId);
|
||||
if (parent == null) {
|
||||
continue;
|
||||
}
|
||||
if ("online".equalsIgnoreCase(parent.getStatus())) {
|
||||
device.setStatus("online");
|
||||
} else if (!StringUtils.hasText(device.getStatus())
|
||||
|| "unknown".equalsIgnoreCase(device.getStatus())) {
|
||||
device.setStatus("offline");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量回填设备所属产品名称与业务编码(供后台设备列表展示"所属产品"列)。
|
||||
*/
|
||||
|
||||
@ -15,8 +15,13 @@
|
||||
-- 品类字典只是"可选值来源",避免历史数据与物模型解析受影响。
|
||||
--
|
||||
-- 无存储过程、无 DELIMITER;幂等可重复执行。
|
||||
--
|
||||
-- 注意:本脚本含中文字面量,首行必须 SET NAMES utf8mb4 —— 服务器 mysql 客户端在 C locale 下
|
||||
-- character_set_client 默认 latin1,缺少该设置会把 UTF-8 字节按 latin1 解读,导致中文双重编码乱码。
|
||||
-- =====================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ---------- 1) 品类字典表 ----------
|
||||
CREATE TABLE IF NOT EXISTS product_category (
|
||||
id BIGINT NOT NULL COMMENT '主键',
|
||||
|
||||
32
backend/sql/V26__fix_mojibake_chinese.sql
Normal file
32
backend/sql/V26__fix_mojibake_chinese.sql
Normal file
@ -0,0 +1,32 @@
|
||||
-- =====================================================================
|
||||
-- V26 · 修复品类字典中文乱码(双重编码)
|
||||
-- ---------------------------------------------------------------------
|
||||
-- 现象:后台「品类管理」品类名称显示为 "煉明" 之类乱码。
|
||||
--
|
||||
-- 成因:服务器 mysql 客户端运行在 C locale,character_set_client 默认 latin1
|
||||
-- (容器内 `SELECT @@character_set_client` → latin1)。
|
||||
-- 迁移脚本经 Windows `type | plink` 管道喂给客户端时,UTF-8 字节被当作
|
||||
-- latin1 字符接收,再按 utf8mb4 列存储 → 中文变成双重编码。
|
||||
-- (历史数据如 product.name 中文正常,是因为当时用 docker cp 进容器再执行,
|
||||
-- 没走管道。)
|
||||
--
|
||||
-- 处理:
|
||||
-- 1) 本脚本首行 SET NAMES utf8mb4,保证脚本里的中文字面量按 utf8mb4 解释;
|
||||
-- 2) 直接按 code 写回正确的中文名称与备注(幂等:仅在值不一致时才更新);
|
||||
-- 3) 后续所有含中文的迁移脚本都需在首行加 SET NAMES utf8mb4,
|
||||
-- deploy-all.bat 也已为 mysql 客户端补 --default-character-set=utf8mb4。
|
||||
--
|
||||
-- 无存储过程、无 DELIMITER;幂等可重复执行。
|
||||
-- =====================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
UPDATE product_category SET name = '照明', remark = '灯/灯泡/灯带', update_time = NOW() WHERE deleted = 0 AND code = 'light';
|
||||
UPDATE product_category SET name = '开关/插座', remark = '开关/插座/墙面面板', update_time = NOW() WHERE deleted = 0 AND code = 'switch';
|
||||
UPDATE product_category SET name = '空调/暖通', remark = '空调/风扇/取暖器', update_time = NOW() WHERE deleted = 0 AND code = 'ac';
|
||||
UPDATE product_category SET name = '窗帘', remark = '窗帘/遮阳', update_time = NOW() WHERE deleted = 0 AND code = 'curtain';
|
||||
UPDATE product_category SET name = '传感器', remark = '温湿度/人体/门磁', update_time = NOW() WHERE deleted = 0 AND code = 'sensor';
|
||||
UPDATE product_category SET name = '门锁', remark = '智能门锁', update_time = NOW() WHERE deleted = 0 AND code = 'lock';
|
||||
UPDATE product_category SET name = '安防', remark = '报警器/摄像头', update_time = NOW() WHERE deleted = 0 AND code = 'security';
|
||||
UPDATE product_category SET name = '其它', remark = '未归类', update_time = NOW() WHERE deleted = 0 AND code = 'other';
|
||||
|
||||
@ -45,10 +45,10 @@ echo [3/8] 应用未执行的 SQL 迁移(%BACKEND_DIR%\sql\V*.sql,按 db_mig
|
||||
echo ============================================
|
||||
set MIG_DIR=%BACKEND_DIR%\sql
|
||||
set CHK_FILE=%TEMP%\_mig_chk.txt
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql -uroot -p'IoT.p2026.Xk9Qm' iot_platform -e \"CREATE TABLE IF NOT EXISTS db_migrations_applied (name VARCHAR(80) NOT NULL PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP)\"" 2>nul
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --default-character-set=utf8mb4 -uroot -p'IoT.p2026.Xk9Qm' iot_platform -e \"CREATE TABLE IF NOT EXISTS db_migrations_applied (name VARCHAR(80) NOT NULL PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP)\"" 2>nul
|
||||
REM 首次使用(tracking 表为空)则标记当前所有 V 文件为已应用:假定目标库已手工迁移到最新。
|
||||
REM 全新库请先手动执行 sql/ 下全部 V 迁移,再运行本脚本(或清空 db_migrations_applied 让其补跑,需迁移文件幂等)。
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql -uroot -p'IoT.p2026.Xk9Qm' iot_platform -N -e \"SELECT COUNT(*) FROM db_migrations_applied\"" 2>nul > "%CHK_FILE%"
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --default-character-set=utf8mb4 -uroot -p'IoT.p2026.Xk9Qm' iot_platform -N -e \"SELECT COUNT(*) FROM db_migrations_applied\"" 2>nul > "%CHK_FILE%"
|
||||
set /p MIG_CNT=<"%CHK_FILE%"
|
||||
if "!MIG_CNT!"=="0" (
|
||||
echo 首次使用:标记当前所有 V 迁移为已应用(假定库已迁移到最新)
|
||||
@ -118,12 +118,12 @@ goto :eof
|
||||
|
||||
:: ============ 迁移子例程(顶层执行,避开括号块内转义问题) ============
|
||||
:mig_seed
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql -uroot -p'IoT.p2026.Xk9Qm' iot_platform -e \"INSERT IGNORE INTO db_migrations_applied(name) VALUES('%1')\"" 2>nul
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --default-character-set=utf8mb4 -uroot -p'IoT.p2026.Xk9Qm' iot_platform -e \"INSERT IGNORE INTO db_migrations_applied(name) VALUES('%1')\"" 2>nul
|
||||
goto :eof
|
||||
|
||||
:mig_apply
|
||||
set CHK=0
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql -uroot -p'IoT.p2026.Xk9Qm' iot_platform -N -e \"SELECT COUNT(*) FROM db_migrations_applied WHERE name='%1'\"" 2>nul > "%CHK_FILE%"
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --default-character-set=utf8mb4 -uroot -p'IoT.p2026.Xk9Qm' iot_platform -N -e \"SELECT COUNT(*) FROM db_migrations_applied WHERE name='%1'\"" 2>nul > "%CHK_FILE%"
|
||||
set /p CHK=<"%CHK_FILE%"
|
||||
if not "!CHK!"=="0" (
|
||||
echo 跳过已应用 %1
|
||||
@ -131,7 +131,7 @@ if not "!CHK!"=="0" (
|
||||
)
|
||||
echo 应用 %1 ...
|
||||
REM --force: 迁移脚本中"已存在"的 DDL(重复列/索引)属正常情况,不应中断整体部署
|
||||
type "%MIG_DIR%\%1" | %PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --force -uroot -p'IoT.p2026.Xk9Qm' iot_platform"
|
||||
type "%MIG_DIR%\%1" | %PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --force --default-character-set=utf8mb4 -uroot -p'IoT.p2026.Xk9Qm' iot_platform"
|
||||
if !ERRORLEVEL! NEQ 0 ( echo [错误] 迁移 %1 执行失败。 & pause & exit /b 1 )
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql -uroot -p'IoT.p2026.Xk9Qm' iot_platform -e \"INSERT IGNORE INTO db_migrations_applied(name) VALUES('%1')\"" 2>nul
|
||||
%PLINK% %PLINK_OPTS% "docker exec -i iot-mysql mysql --default-character-set=utf8mb4 -uroot -p'IoT.p2026.Xk9Qm' iot_platform -e \"INSERT IGNORE INTO db_migrations_applied(name) VALUES('%1')\"" 2>nul
|
||||
goto :eof
|
||||
|
||||
@ -85,8 +85,12 @@ class DeviceService {
|
||||
name: item.deviceName || item.name,
|
||||
type: item.deviceType || item.type || 'default',
|
||||
category: item.category || 'default',
|
||||
online: item.status === 'ONLINE' || item.online,
|
||||
status: this.parseStatus(item.statusData || item.status),
|
||||
// 后端状态是小写的 online/offline(网关下挂子设备可能为 unknown),
|
||||
// 这里统一按小写比较并兼容布尔值;此前比的是大写 'ONLINE',导致设备恒显示离线。
|
||||
online: String(item.status ?? '').toLowerCase() === 'online' || item.online === true,
|
||||
// status 字段是状态字符串而非 DP JSON,不能拿去做 JSON 解析;
|
||||
// 真正的 DP 在 capabilities.dps 里。
|
||||
status: this.parseStatus(item.capabilities?.dps || item.statusData || {}),
|
||||
platform: 'local' as const,
|
||||
icon: item.icon,
|
||||
roomId: item.roomId,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user