diff --git a/backend/iot-device/src/main/java/com/iot/device/mapper/AiDeviceProfileMapper.java b/backend/iot-device/src/main/java/com/iot/device/mapper/AiDeviceProfileMapper.java index 4e2c596..d77530d 100644 --- a/backend/iot-device/src/main/java/com/iot/device/mapper/AiDeviceProfileMapper.java +++ b/backend/iot-device/src/main/java/com/iot/device/mapper/AiDeviceProfileMapper.java @@ -5,6 +5,7 @@ import com.iot.device.entity.AiDeviceProfile; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; /** * AI 设备控制档案 Mapper(iot-device 侧写入) @@ -27,4 +28,20 @@ public interface AiDeviceProfileMapper extends BaseMapper { + "AND deleted = 0 LIMIT 1") AiDeviceProfile selectByProductAndOwner(@Param("productId") Long productId, @Param("ownerId") Long ownerId); + + /** + * 逻辑删除指定用户的 AI 档案(解绑涂鸦账号时联动失效) + *

+ * 档案里的指令码(power_code 等)是按涂鸦 DP 识别的,设备失效后这些档案不再可用; + * 保留会指向已失效设备,重新绑定后应由 AI 档案检测流程重新生成。 + *

+ * 按 owner_id 失效而非 JOIN device 比较 device_id:ai_device_profile.device_id 与 + * device.device_id 的排序规则不同(utf8mb4_unicode_ci vs utf8mb4_0900_ai_ci), + * 跨表字符串比较会报 ERROR 1267 Illegal mix of collations。 + * + * @return 被失效的档案数量 + */ + @Update("UPDATE ai_device_profile SET deleted = 1, update_time = NOW() " + + "WHERE deleted = 0 AND owner_id = #{ownerId}") + int invalidateProfilesByOwner(@Param("ownerId") Long ownerId); } diff --git a/backend/iot-device/src/main/java/com/iot/device/mapper/DeviceMapper.java b/backend/iot-device/src/main/java/com/iot/device/mapper/DeviceMapper.java index d41e31b..291bd95 100644 --- a/backend/iot-device/src/main/java/com/iot/device/mapper/DeviceMapper.java +++ b/backend/iot-device/src/main/java/com/iot/device/mapper/DeviceMapper.java @@ -44,4 +44,21 @@ public interface DeviceMapper extends BaseMapper { */ @Update("UPDATE device SET deleted = 0 WHERE id = #{id}") int restoreById(@Param("id") Long id); + + /** + * 逻辑删除指定用户的全部涂鸦设备(解绑涂鸦账号时联动失效) + *

+ * 背景:解绑涂鸦账号后,绑定记录被物理删除,但同步来的设备仍留在设备列表, + * 形成"孤儿涂鸦设备"——设备无法再通过涂鸦云端控制,却仍会被联动规则命中, + * 每条规则触发都会产生一次失败指令与错误日志。 + *

+ * 这里做逻辑删除(deleted=1)而非物理删除:device 表 uk_device_id 唯一索引不含 + * deleted,物理删除还会让 device_state / device_activity_log 等历史记录悬空; + * 用户重新绑定并同步时,restoreById 会把记录自动恢复。 + * + * @return 被失效的设备数量 + */ + @Update("UPDATE device SET deleted = 1, update_time = NOW() " + + "WHERE deleted = 0 AND platform = 'tuya' AND owner_id = #{ownerId}") + int invalidateTuyaDevicesByOwner(@Param("ownerId") Long ownerId); } diff --git a/backend/iot-device/src/main/java/com/iot/device/service/TuyaCloudService.java b/backend/iot-device/src/main/java/com/iot/device/service/TuyaCloudService.java index 9920312..933afab 100644 --- a/backend/iot-device/src/main/java/com/iot/device/service/TuyaCloudService.java +++ b/backend/iot-device/src/main/java/com/iot/device/service/TuyaCloudService.java @@ -8,6 +8,7 @@ import com.iot.device.dto.tuya.*; import com.iot.device.entity.Device; import com.iot.device.entity.PlatformConfig; import com.iot.device.entity.TuyaUserBinding; +import com.iot.device.mapper.AiDeviceProfileMapper; import com.iot.device.mapper.DeviceMapper; import com.iot.device.mapper.PlatformConfigMapper; import com.iot.device.mapper.TuyaUserBindingMapper; @@ -45,6 +46,7 @@ public class TuyaCloudService { private final PlatformConfigMapper platformConfigMapper; private final TuyaUserBindingMapper tuyaUserBindingMapper; private final DeviceMapper deviceMapper; + private final AiDeviceProfileMapper aiDeviceProfileMapper; private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper = new ObjectMapper(); @@ -435,11 +437,20 @@ public class TuyaCloudService { /** * 解绑涂鸦账号(物理删除,避免唯一索引冲突) + *

+ * 同时联动失效该用户同步来的涂鸦设备与 AI 档案: + * 解绑后这些设备已无法通过涂鸦云端控制,若继续留在设备列表并被联动规则命中, + * 每次规则触发都会产生一条失败指令与错误日志(历史问题:孤儿涂鸦设备)。 + * 设备采用逻辑删除,重新绑定并同步时会被自动恢复。 */ public void unbindUser(Long memberId) { // 先删除该用户所有绑定记录(防止有重复行导致唯一索引冲突) tuyaUserBindingMapper.deleteByMemberId(memberId); - log.info("会员 {} 解绑了涂鸦账号(物理删除)", memberId); + // 联动失效:涂鸦设备 + 指向它们的 AI 档案 + int invalidatedDevices = deviceMapper.invalidateTuyaDevicesByOwner(memberId); + int invalidatedProfiles = aiDeviceProfileMapper.invalidateProfilesByOwner(memberId); + log.info("会员 {} 解绑了涂鸦账号(物理删除),联动失效涂鸦设备 {} 个、AI 档案 {} 条", + memberId, invalidatedDevices, invalidatedProfiles); } // ==================== SMS验证码绑定(正式模式 - 调用涂鸦API) ==================== diff --git a/backend/sql/V24__cleanup_orphan_tuya_devices.sql b/backend/sql/V24__cleanup_orphan_tuya_devices.sql new file mode 100644 index 0000000..9ce48f4 --- /dev/null +++ b/backend/sql/V24__cleanup_orphan_tuya_devices.sql @@ -0,0 +1,51 @@ +-- ===================================================================== +-- V24 · 清理测试阶段遗留的孤儿涂鸦设备与测试联动数据 +-- --------------------------------------------------------------------- +-- 背景: +-- 1) unbindUser 早期只物理删除 tuya_user_binding 绑定记录,不处理已同步的设备, +-- 于是产生"孤儿涂鸦设备":设备仍在设备列表,却已无涂鸦账号可控制。 +-- 定时场景每分钟命中的联动规则会持续对它们下发指令,全部失败并刷错误日志。 +-- (代码侧已修复:解绑时联动逻辑删除设备与 AI 档案,重新绑定同步时自动恢复) +-- 2) 测试阶段遗留的联动规则与场景:条件全部相同(switch_1 == true)但动作互相矛盾 +-- (switch_1=true / switch_1=0 / {"1":"open"} 混用 dpId),会下发自相矛盾的指令。 +-- +-- 清理范围: +-- - 逻辑删除孤儿涂鸦设备(owner 已无涂鸦绑定记录) +-- - 逻辑删除指向这些设备的 AI 档案 +-- - 删除测试联动规则与测试场景(含每分钟 cron 的 bb 场景) +-- 保留:产品与物模型(product / product_property / product_event / product_action), +-- 家庭与成员、设备活动日志(历史审计)等。 +-- +-- 无存储过程、无 DELIMITER;幂等可重复执行。 +-- ===================================================================== + +-- ---------- 1) 逻辑删除孤儿涂鸦设备 ---------- +UPDATE device d + SET d.deleted = 1, + d.update_time = NOW() + WHERE d.deleted = 0 + AND d.platform = 'tuya' + AND d.owner_id > 0 + AND NOT EXISTS (SELECT 1 FROM tuya_user_binding b WHERE b.member_id = d.owner_id); + +-- ---------- 2) 逻辑删除已解绑用户的 AI 档案 ---------- +-- 注意:这里按 owner_id 判定,而不是 JOIN device 比较 device_id—— +-- ai_device_profile.device_id 与 device.device_id 的排序规则不同 +-- (utf8mb4_unicode_ci vs utf8mb4_0900_ai_ci),跨表字符串比较会报 +-- ERROR 1267 Illegal mix of collations。owner_id 为数值列,无此问题。 +UPDATE ai_device_profile a + SET a.deleted = 1, + a.update_time = NOW() + WHERE a.deleted = 0 + AND a.owner_id > 0 + AND NOT EXISTS (SELECT 1 FROM tuya_user_binding b WHERE b.member_id = a.owner_id); + +-- ---------- 3) 删除测试联动规则 ---------- +DELETE FROM linkage_rule WHERE deleted = 0; + +-- ---------- 4) 删除测试场景(bb 场景 cron 为每分钟执行) ---------- +DELETE FROM scene WHERE deleted = 0; + +-- ---------- 5) 清理场景执行记录 ---------- +DELETE FROM scene_run; +