9 Commits

Author SHA1 Message Date
2995840376
066d374673 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。
2026-10-28 01:25:46 +08:00
2995840376
903b44c84b feat(L): 新增品类管理, 收口涂鸦设备写入的归属校验
品类管理: 新建 product_category 品类字典表(V25, 灌入 8 个预设品类并回填存量 product.category), 后端提供 ProductCategory 增删改查/启停/字典接口, 后台新增「品类管理」页面(设备管理组, 与产品管理同级), 产品管理的品类下拉改为读字典(失败回退内置枚举)。

孤儿设备根治: TuyaController.bindDevices 收口三道校验 —— 绑定门禁(本平台未绑定涂鸦账号直接拒绝)、归属白名单(上报设备须在该用户涂鸦账号真实清单内, 拉取失败降级放行)、owner 校验(他人名下设备不改写; 恢复已删设备时归属改到当前用户)。

兜底自愈: 新增 OrphanTuyaDeviceCleanupTask, 每天 04:30 回收无绑定记录用户的涂鸦设备与 AI 档案(逻辑删除, 可恢复)。

Android: 查看设备时的补同步先确认后端绑定状态, 未绑定不再写库; 同步被拒时记录原因而非静默失败。单测 TuyaControllerBindTest 5 用例覆盖门禁/白名单/owner/恢复归属。
2026-10-28 01:10:59 +08:00
2995840376
4c2d92571e feat(K): 规则保存时校验动作参数必须用物模型 code, 禁止 dpId
背景: 规则动作 params 的 key 会被原样当作涂鸦 code 下发(dp_id 从不参与下发), 历史测试数据里出现过 switch_1 与 1 混用, 导致下发失败或指令相互矛盾, 且保存时无任何校验。

后端: 新增 RuleActionValidator, createRule/updateRule 前校验动作参数 key 必须命中 product_property.code; 命中 dp_id 时直接提示应改用的 code; 同步校验值域(bool 取值、数值 min/max 区间、enum 候选值); 目标设备不存在、无产品物模型等场景给出明确业务错误。

前端: 后台规则表单补齐执行动作编辑(JSON), 提交前本地解析校验, 原本表单不提交 actions 导致新建规则必然失败。

Android: 物模型不可用时的回退路径改为优先使用涂鸦 code(反射读取), 避免提交 dpId 被后端拒绝。

V24: scene_run 表不存在时容错跳过, 便于在未执行 V16 的旧结构库上整段执行。
2026-10-28 00:52:04 +08:00
2995840376
91d50eda91 fix(J): 清理孤儿涂鸦设备与测试联动数据, 解绑时联动失效设备与AI档案
数据层(V24 迁移): 逻辑删除无涂鸦绑定记录的孤儿涂鸦设备及其AI档案; 物理删除测试阶段的联动规则、场景与场景执行记录(条件相同但动作矛盾的测试数据); 保留产品/物模型/家庭/成员/活动日志。幂等可重复执行。

代码层(根治): TuyaCloudService.unbindUser 解绑时联动逻辑删除该用户的涂鸦设备与AI档案, 避免解绑后设备仍在列表被联动规则反复命中而刷错误日志; 采用逻辑删除, 用户重新绑定同步时 restoreById 自动恢复。
2026-10-27 21:56:45 +08:00
2995840376
0ff9103089 fix(H): 修复 product_property 驼峰遗留列导致后台产品页面 500
问题:后台「产品中心 - 产品列表/详情/物模型」报
      Unknown column 'completion_mode' in 'field list'(BadSqlGrammarException)

根因:V19(面板 P2 迁移)建列时把 completionMode 直接当作列名写入 product_property(驼峰),
      而 MyBatis-Plus 默认把实体字段 completionMode 映射为下划线形式 completion_mode,
      实体查询/写入时找不到列。
      注:V22 已把 completion_mode 的权威归属判定为 product_action 维度,
      product_property 上的同名列属于历史兼容,保留但统一为下划线命名。

修复:新增 V23 迁移,将 product_property.completionMode 重命名为 completion_mode
  - 幂等可重复执行,三种情形分别处理:
      1) 仅有驼峰列   -> 直接 RENAME COLUMN
      2) 两列都存在   -> 先回填旧列非空值到新列,再 DROP 旧列
      3) 仅有下划线列 -> 跳过
  - 无存储过程、无 DELIMITER(MySQL batch 管道模式下 DELIMITER 失效)
  - 全库扫描(COLUMN_NAME <> LOWER(COLUMN_NAME))确认驼峰遗留列仅此一处

上线过程:全库备份 /opt/iotsaas/backup/iot_platform_before_v23.sql
          -> 克隆库 iot_v23_test 演练两遍(重命名成功 + 幂等无副作用)-> 生产执行 -> 克隆库 DROP

验证:/api/v1/products、/api/v1/products/{id}、/api/v1/products/{id}/spec、
      /api/v1/products/{id}/control-schema 全部返回 200,物模型属性正常返回。
2026-10-27 19:12:13 +08:00
2995840376
6e3f3ea661 fix(E): 修复 iot-device 启动失败 + V22 迁移脚本重写为可执行版本
【生产故障与修复】
iot-device 部署后反复重启失败(status=1/FAILURE, restart counter 3+):
  UnsatisfiedDependencyException: No qualifying bean of type
  'com.iot.device.mapper.ProductActionMapper' available
根因: iot-device 模块【没有 @MapperScan】(仅 iot-rule 有), Mapper 靠
      @Mapper 注解被 mybatis-spring-boot-starter 自动扫描;
      新建的 ProductEventMapper / ProductActionMapper 漏写 @Mapper 注解,
      导致扫描不到 -> ControlSchemaServiceImpl 注入失败 -> 启动失败。
修复: 两个 Mapper 补 @Mapper 注解; 已全量核查 iot-device 31 个 Mapper 均有该注解。

【V22 迁移脚本重写】(实测发现原写法在本环境根本执行不了)
- 原写法使用存储过程 + DELIMITER, 实测三种执行方式(管道/文件重定向/source)
  全部失败: MySQL 客户端在 batch 模式会把 DELIMITER 当普通 SQL 发给服务器。
  生产库现存存储过程数为 0, 佐证历史 V19/V21 等含 DELIMITER 的脚本
  其存储过程部分从未生效(仅 ADD COLUMN 等语句生效)。
- 重写为零存储过程、零 DELIMITER 的普通语句序列:
  加列幂等 -> PREPARE + information_schema 判定(ADD COLUMN 走 PREPARE 实测可用)
  索引     -> 直接 DDL(ADD INDEX 走 PREPARE 实测报 1064, 直接执行可用)
- 库名统一用 DATABASE() 判定, 测试库克隆无需改写脚本
- 清理顺序调整: 唯一索引统一放在软删重复行清理之后, 避免索引创建失败

【生产验证】(R730 192.168.3.3)
- 全库备份: /opt/iotsaas/backup/iot_platform_before_v22.sql
- 克隆库演练: 建克隆库 -> 跑 V22 -> 校验 -> DROP, 生产数据零风险
- 正式执行: platform_type 8行 / product.protocol / product_property.grp,
  is_primary, enum_label / product_action.completion_mode 全部就位
- 唯一索引: uk_platform_product / uk_product_code(x3) / idx_product_id 全部就位
- 数据完整性: product=11 device=13, 与迁移前一致
- 数据清洗: 脏 model/manufacturer=0, 空串 platform_product_id=0, device.miot=0
- tracking 表已标记 V22, 避免 deploy 脚本重复执行

【部署脚本加固】
deploy-all.bat 迁移步骤加 --force: 迁移脚本中'已存在'的 DDL(重复列/索引)
属正常情况, 不应中断整体部署

验证: 4 服务全部 active; 后台首页/SPA路由/9081文档/9083文档 均 200;
产品列表 API 返回 401 未认证(路由与过滤器正常)
2026-10-27 13:49:15 +08:00
2995840376
d6d9a94736 feat(C2-C4): 物模型编辑器支持语义编辑 + 修复全量替换撞唯一索引
SpecEditor.vue:
- 属性表新增 语义/分组/主开关 三列
- 属性编辑表单新增 semantic 下拉(17种标签, 与后端 SpecSemanticResolver 一一对应)、
  grp 下拉(8种分组)、is_primary 开关
- 分组说明写明: switch 组的开关位支持整组联动(switch_1~4 一起开关)
- 语义说明写明: 决定客户端如何渲染与能否作为主开关, 取代按名称猜测

后端 saveSpec 修复:
- 全量替换由逻辑删除改为物理删除
  原因: 逻辑删除留下同 (product_id, code) 的软删行, 与 V22 的
  (product_id, code, deleted) 唯一索引冲突 —— 连续保存两次即报错
- 新增 ProductEventMapper/ProductActionMapper.physicallyDeleteByProductId
- 语义缺失时自动推导(与 SpecSemanticResolver 规则一致)
- 状态量被误标 write 时强制只读
- 保存后调用 applyPrimary 重算主开关
- action 缺省 completionMode=SYNC

applyPrimary 语义修正:
- 人工优先: 已存在人工标记(is_primary=1)则保留人工选择, 不自动改写
- 仅在无人工标记时按语义规则自动补齐
  (reconcile 场景重建后无标记 -> 自动选; 后台编辑场景有人工标记 -> 尊重)

V22 迁移顺序修正:
- 唯一索引统一移到'软删重复行清理'之后执行, 避免索引创建即失败
- 新增软删重复行物理清理(同 (product_id, code) 只保留 id 最小的一条)
- 索引语句去重

验证: mvn BUILD SUCCESS; vite build 通过(ProductDetail chunk 已更新)
2026-10-27 13:19:08 +08:00
2995840376
a932633c50 refactor(A): 产品/物模型/设备三层架构重构 - 地基批次
新增:
- iot-common/PlatformType: 平台维度单一真相源(枚举)
  统一 tuya/matter/local/xiaomi/huawei/dueros/alexa/google_home,
  提供 legacy 别名归一(miot|mi -> xiaomi)与已知性判定
- backend/sql/V22__product_device_arch_refactor.sql: 幂等迁移(153行)
  A. platform_type 平台字典表 + 种子数据(后台可维护)
  B. product 加 protocol 列; platform_product_id 空串清洗为 NULL;
     清洗 model/manufacturer 被涂鸦 productId 污染的历史脏数据;
     加 (platform, platform_product_id, deleted) 唯一约束
  C. product_property 加 grp/is_primary/enum_label + (product_id,code) 唯一约束
  D. product_event/product_action 加 (product_id,code) 唯一约束;
     product_action 加 completion_mode
  E. device 加 product_id 索引(OTA 物化目标设备此前全表扫); platform 归一

改造:
- DeviceAdapter 新增 platform() 显式声明平台
- DeviceAdapterManager 改为按 platform() 精确匹配,
  移除'类名是否包含平台字符串'的脆弱约定(类名与平台码不同名会静默降级)
- DevicePlatform 委托 PlatformType, 消除第四套平台取值域

验证: mvn -pl iot-common,iot-device -am compile 通过
2026-10-27 12:08:44 +08:00
2995840376
0ce191c448 chore: 初始化 iotsaas 版本库基线
纳入范围:
- backend/  Spring Boot 多模块后端(iot-common/device/rule/data/alarm/auth)
- frontend/admin/  Vue3 + Element Plus 管理后台
- Android/   涂鸦原生 APP (com.xhai.iot)
- backend/sql/  Flyway 风格迁移脚本(V2~V21)
- docs/       对接文档与设计文档

排除范围:
- 构建产物: node_modules/ target/ build/ dist/ .gradle/
- 签名密钥: *.jks *.keystore (严禁入库)
- 本机配置: Android/local.properties
- 运行日志: logs/ *.log
- AI 协作本地数据 .codebuddy/ 与第三方 demo tuya-demo/

.gitattributes 统一以 LF 存储, Windows 批处理保留 CRLF
2026-10-27 12:00:41 +08:00