feat(device): 一期设备域 I1-I5 全量实现(APP 接口/后台管理/OTA/MQTT/直播)

模块接入:
- 新增 ruoyi-device(org.dromara.device)并注册到根/ruoyi-modules/ruoyi-admin pom

APP 侧(/app/**,需登录,仅本人绑定设备):
- 账号闭环:短信/邮箱验证码注册登录、密码重置(不跨界返回散列)
- 设备:绑定/列表/详情/重命名/解绑(越权统一"设备不存在",防 IDOR)
- 眼罩开关(/app/device/{deviceNo}/hood,低电不对称拦截)
- 定位(/app/device/{deviceNo}/location)
- 直播(/app/stream 6 端点 + /app/device/{deviceNo}/live,自建 ZLMediaKit,设备主动推流)

后台(/device/**):
- 设备 CRUD/统计/删除/导出(ExcelBuilder)
- OTA(6 端点:升级包管理 + 批量下发,total==created+skipped+failed)

MQTT(T05):
- 遥测/事件/ack 监听、指令下发+回执(防串设备)、离线扫描、低电告警
- 报文身份仅来自 Topic,反查 device_id 防跨设备伪造

数据与脚本:
- script/sql/ry_device.sql(6 表/字典/权限字/菜单)、bm_patch.sql
- script/mqtt 设备模拟器(mqtt-simulate.js)

安全:
- MQTT broker 凭据外置为环境变量 MQTT_USERNAME/MQTT_PASSWORD,不落明文
- ZLM webhook 密钥外置为环境变量 ZLM_SECRET(未配置时 /zlm/hook/** 失败关闭),不落明文
This commit is contained in:
yuhaiming
2026-09-17 10:10:38 +08:00
parent 926426fe4f
commit 79b739fe02
142 changed files with 14627 additions and 7 deletions

View File

@@ -0,0 +1,137 @@
package org.dromara.system.api;
import org.dromara.system.api.domain.AppUserDTO;
/**
* APP 用户(C 端)领域服务对外暴露的 API。
* <p>
* 设计口径(迭代 I2 边界裁定 · 方案 A):
* <ul>
* <li>本接口仅面向设备域({@code ruoyi-device})等业务模块,用于 APP 账号闭环(注册/登录/改密/换绑/注销)。</li>
* <li><b>密码绝不跨模块边界</b>:本接口不返回、不接收任何已加密的密码散列;
* 校验密码由 {@link #matchesPassword(Long, String)} 在 {@code ruoyi-system} 内部完成并只回传布尔值,
* 写入密码由 {@link #registerByPhone(String, String, String)} / {@link #resetPassword(Long, String)}
* 在 {@code ruoyi-system} 内部完成散列(BCrypt)后落库。</li>
* <li>本接口为<b>纯新增</b>,不改动既有 {@link UserService} 及 {@code ruoyi-system} 中任何既有方法/行为。</li>
* <li>复用 {@code sys_user} 表,C 端账号 {@code user_type = app_user};不新增表、不新增字段。</li>
* </ul>
*
* @author Kou
* @date 2026-09-16
*/
public interface AppUserApiService {
/**
* 按手机号查询 APP 用户(仅匹配 {@code user_type = app_user} 且未逻辑删除的账号)。
*
* @param phoneNumber 手机号
* @return APP 用户信息(不含密码);不存在时返回 {@code null}
*/
AppUserDTO selectByPhone(String phoneNumber);
/**
* 判断手机号是否已被注册为 APP 用户(仅匹配 {@code user_type = app_user})。
*
* @param phoneNumber 手机号
* @return true 已注册;false 未注册
*/
boolean existsByPhone(String phoneNumber);
/**
* 校验用户密码是否匹配。
* <p>
* 明文密码仅在本方法内部比对(BCrypt),比较结果以布尔值回传,
* 密码散列与明文均<b>不</b>跨越模块边界。
*
* @param userId 用户主键
* @param rawPassword 明文密码
* @return true 匹配;false 不匹配或用户不存在
*/
boolean matchesPassword(Long userId, String rawPassword);
/**
* 以手机号注册一个 APP 用户。
* <p>
* 账号用户名取手机号;密码在本方法内部散列后落库。
*
* @param phoneNumber 手机号
* @param rawPassword 明文密码
* @param nickName 昵称(为空时默认取手机号)
* @return 新建用户主键
*/
Long registerByPhone(String phoneNumber, String rawPassword, String nickName);
/**
* 按邮箱查询 APP 用户(仅匹配 {@code user_type = app_user} 且未逻辑删除的账号)。
* <p>
* 邮箱比对口径:调用方须先做「去空白 + 转小写」归一化(见
* {@code org.dromara.device.email.AppEmailService#normalize}),本方法按传入值精确匹配。
*
* @param email 邮箱地址(已归一化)
* @return APP 用户信息(不含密码);不存在时返回 {@code null}
*/
AppUserDTO selectByEmail(String email);
/**
* 判断邮箱是否已被注册为 APP 用户(仅匹配 {@code user_type = app_user})。
*
* @param email 邮箱地址(已归一化)
* @return true 已注册;false 未注册
*/
boolean existsByEmail(String email);
/**
* 以邮箱注册一个 APP 用户(邮箱注册入口)。
* <p>
* <b>不写手机号</b>:本入口创建的账号 {@code phone_number} 为 {@code NULL}
* (依赖 {@code script/sql/bm_patch.sql} 补丁 3 把该列默认值改为 NULL,
* 否则 MyBatis-Plus 的 {@code insertStrategy: NOT_NULL} 会省略该列、落库为 {@code ''},
* 导致第二个邮箱账号撞 {@code uk_sys_user_phone_number})。账号须在后续流程中绑定手机号。
* <p>
* 密码在本方法内部散列后落库。
*
* @param email 邮箱地址(已归一化)
* @param rawPassword 明文密码
* @param nickName 昵称(为空时默认取邮箱)
* @return 新建用户主键
*/
Long registerByEmail(String email, String rawPassword, String nickName);
/**
* 重置用户密码(忘记密码场景)。
*
* @param userId 用户主键
* @param rawPassword 新明文密码
* @return true 修改成功
*/
boolean resetPassword(Long userId, String rawPassword);
/**
* 换绑手机号。
*
* @param userId 用户主键
* @param newPhoneNumber 新手机号
* @return true 修改成功
*/
boolean updatePhone(Long userId, String newPhoneNumber);
/**
* 停用账号(注销前置:解绑设备后停用)。
*
* @param userId 用户主键
* @return true 停用成功
*/
boolean disableUser(Long userId);
/**
* 判断账号是否已绑定手机号。
* <p>
* 供设备域做「邮箱注册后仍须绑手机号」的服务端硬拦截使用
* (未绑手机号的账号不允许绑定设备)。
*
* @param userId 用户主键
* @return true 已绑定非空手机号;false 未绑定或用户不存在
*/
boolean isPhoneBound(Long userId);
}

View File

@@ -0,0 +1,68 @@
package org.dromara.system.api.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* APP 用户传输对象。
* <p>
* 面向 APP 账号闭环的对外 DTO,<b>刻意不包含密码字段</b>,
* 以确保密码散列不会跨越模块边界(见 {@link org.dromara.system.api.AppUserApiService})。
*
* @author Kou
* @date 2026-09-16
*/
@Data
@NoArgsConstructor
public class AppUserDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 用户账号(APP 账号取手机号)
*/
private String userName;
/**
* 用户昵称
*/
private String nickName;
/**
* 手机号码
*/
private String phoneNumber;
/**
* 用户邮箱
* <p>
* 仅邮箱注册的账号非空;手机号注册的账号为 {@code null}。
*/
private String email;
/**
* 用户类型(固定为 app_user)
*/
private String userType;
/**
* 账号状态(0 正常 1 停用)
*/
private String status;
/**
* 创建时间
*/
private LocalDateTime createTime;
}