From 066d374673df7c3f98c8b9ee4a345c7b92ca6376 Mon Sep 17 00:00:00 2001 From: 2995840376 <2995840376@xiaomi.com> Date: Wed, 28 Oct 2026 01:25:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(M):=20=E4=BF=AE=E5=A4=8D=E5=93=81=E7=B1=BB?= =?UTF-8?q?=E4=B8=AD=E6=96=87=E4=B9=B1=E7=A0=81,=20=E5=AD=90=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E5=9C=A8=E7=BA=BF=E7=8A=B6=E6=80=81=E6=8C=89=E7=88=B6?= =?UTF-8?q?=E7=BD=91=E5=85=B3=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 乱码: 服务器 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。 --- .../java/com/xhai/iot/model/TuyaDevice.java | 12 ++++ .../java/com/xhai/iot/ui/DevicesFragment.java | 20 ++++-- .../xhai/iot/ui/adapter/DeviceRowAdapter.java | 22 +++++- .../iot/device/controller/TuyaController.java | 68 +++++++++++++++++-- .../iot/device/dto/tuya/TuyaBindRequest.java | 10 +++ .../service/impl/DeviceServiceImpl.java | 46 +++++++++++++ backend/sql/V25__add_product_category.sql | 5 ++ backend/sql/V26__fix_mojibake_chinese.sql | 32 +++++++++ deploy-all.bat | 12 ++-- uniapp/src/services/deviceService.ts | 8 ++- 10 files changed, 217 insertions(+), 18 deletions(-) create mode 100644 backend/sql/V26__fix_mojibake_chinese.sql diff --git a/Android/app/src/main/java/com/xhai/iot/model/TuyaDevice.java b/Android/app/src/main/java/com/xhai/iot/model/TuyaDevice.java index fc4260e..adf2085 100644 --- a/Android/app/src/main/java/com/xhai/iot/model/TuyaDevice.java +++ b/Android/app/src/main/java/com/xhai/iot/model/TuyaDevice.java @@ -51,6 +51,15 @@ public class TuyaDevice { @SerializedName("parentDeviceId") private String parentDeviceId; + /** + * 后端原始状态值(online / offline / unknown) + * + *

子设备(网关下挂)没有独立在线通道,后端会按父网关聚合状态, + * 无法用 online/offline 两态表达"未知",因此保留原始值供 UI 区分展示。

+ */ + @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; } diff --git a/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java b/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java index f42e0c2..5e71eb2 100644 --- a/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java +++ b/Android/app/src/main/java/com/xhai/iot/ui/DevicesFragment.java @@ -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()); diff --git a/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceRowAdapter.java b/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceRowAdapter.java index 67b4428..e0123c6 100644 --- a/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceRowAdapter.java +++ b/Android/app/src/main/java/com/xhai/iot/ui/adapter/DeviceRowAdapter.java @@ -145,7 +145,7 @@ public class DeviceRowAdapter extends RecyclerView.Adapter后端对网关下挂的子设备会按父网关聚合状态,取不到权威信息时用 unknown + * 表达"状态未知",而不是一律兜底成"离线"(曾导致通电在线的子设备显示离线)。

+ */ + 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 "状态未知"; + } + /** * 绑定胶囊开关与状态文字(抽成独立方法)。 * 局部刷新时只调用这里,不重载图标,避免列表闪烁 diff --git a/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java b/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java index 3b6eacf..2c28914 100644 --- a/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java +++ b/backend/iot-device/src/main/java/com/iot/device/controller/TuyaController.java @@ -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); } + /** + * 计算涂鸦设备的有效在线状态。 + * + *

为什么需要它:涂鸦 Mesh/Zigbee 子设备没有独立的在线通道 —— 涂鸦 SDK 与 + * OpenAPI 快照对它们的 online 恒为 false(实测:通电在线的 JD6BTSA 云端仍为 false)。 + * APP 把这个 false 上报后,device.status 就被写成 offline,用户看到「明明通电、网关 + * 在线,子设备却显示离线」。

+ * + *

判定规则:

+ * + */ + 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"; + } + + /** + * 判断是否为网关设备。 + * + *

子设备一定有父设备,因此「没有父设备」且品类属于网关类(wg/gateway/gw)即为网关。

+ */ + 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); + } + // ==================== 设备控制 ==================== /** diff --git a/backend/iot-device/src/main/java/com/iot/device/dto/tuya/TuyaBindRequest.java b/backend/iot-device/src/main/java/com/iot/device/dto/tuya/TuyaBindRequest.java index ca873a3..1a934a0 100644 --- a/backend/iot-device/src/main/java/com/iot/device/dto/tuya/TuyaBindRequest.java +++ b/backend/iot-device/src/main/java/com/iot/device/dto/tuya/TuyaBindRequest.java @@ -57,5 +57,15 @@ public class TuyaBindRequest { private String productId; @Schema(description = "设备图标URL") private String icon; + /** + * 父设备(网关)ID + * + *

涂鸦 Mesh/Zigbee 子设备没有独立在线通道,涂鸦 SDK 与云端快照对它们的 + * online 恒为 false(实测通电在线的子设备也恒 false)。子设备的在线状态只能 + * 依据「父网关是否在线」判定,因此前端需把涂鸦 SDK 里的父设备关系一并上报, + * 后端据此写入 device.parent_device_id。

+ */ + @Schema(description = "父设备(网关)ID") + private String parentDeviceId; } } diff --git a/backend/iot-device/src/main/java/com/iot/device/service/impl/DeviceServiceImpl.java b/backend/iot-device/src/main/java/com/iot/device/service/impl/DeviceServiceImpl.java index 6d1771f..fe7a17d 100644 --- a/backend/iot-device/src/main/java/com/iot/device/service/impl/DeviceServiceImpl.java +++ b/backend/iot-device/src/main/java/com/iot/device/service/impl/DeviceServiceImpl.java @@ -174,9 +174,55 @@ public class DeviceServiceImpl extends ServiceImpl impleme Page result = page(new Page<>(query.getPageNum(), query.getPageSize()), wrapper); fillProductInfo(result.getRecords()); + resolveSubDeviceStatus(result.getRecords()); return result; } + /** + * 子设备在线状态聚合(仅影响返回结果,不写库)。 + * + *

涂鸦 Mesh/Zigbee 子设备没有独立的在线通道 —— 涂鸦 SDK 与 OpenAPI 快照对它们的 + * online 恒为 false,APP 把这个 false 上报后,库里存的就是 offline。用户明明看到 + * 子设备通电、网关在线,列表里却显示离线。

+ * + *

这里以「父网关在线」为判据做一次聚合:父网关在线 → 子设备按在线展示; + * 父网关不在线且子设备状态未知 → 按离线展示。写入侧同样有对��规则 + * (TuyaController.resolveEffectiveOnlineStatus),这里是展示侧的兜底, + * 保证历史数据也能立即显示正确。

+ */ + private void resolveSubDeviceStatus(List devices) { + if (devices == null || devices.isEmpty()) { + return; + } + Set parentIds = devices.stream() + .map(Device::getParentDeviceId) + .filter(StringUtils::hasText) + .collect(Collectors.toSet()); + if (parentIds.isEmpty()) { + return; + } + Map parentMap = baseMapper.selectList(new LambdaQueryWrapper() + .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"); + } + } + } + /** * 批量回填设备所属产品名称与业务编码(供后台设备列表展示"所属产品"列)。 */ diff --git a/backend/sql/V25__add_product_category.sql b/backend/sql/V25__add_product_category.sql index ac80102..276f4cf 100644 --- a/backend/sql/V25__add_product_category.sql +++ b/backend/sql/V25__add_product_category.sql @@ -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 '主键', diff --git a/backend/sql/V26__fix_mojibake_chinese.sql b/backend/sql/V26__fix_mojibake_chinese.sql new file mode 100644 index 0000000..7deba03 --- /dev/null +++ b/backend/sql/V26__fix_mojibake_chinese.sql @@ -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'; + diff --git a/deploy-all.bat b/deploy-all.bat index 715e96f..bc7672b 100644 --- a/deploy-all.bat +++ b/deploy-all.bat @@ -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 diff --git a/uniapp/src/services/deviceService.ts b/uniapp/src/services/deviceService.ts index e50e489..5fbb788 100644 --- a/uniapp/src/services/deviceService.ts +++ b/uniapp/src/services/deviceService.ts @@ -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,