diff --git a/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml b/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml
index 285a855..b925b42 100644
--- a/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml
+++ b/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application-druid.yml
@@ -4,7 +4,9 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
- # 主库数据源
+ # 主库数据源:这里是兜底默认值(本机);
+ # application-local.yml / application-prod.yml 会按环境覆盖 master 的 url/username/password
+ # (profile 顺序中靠后的生效,默认 druid,local;服务器用 druid,prod)
master:
url: jdbc:mysql://localhost:3306/iotlocks?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: iotlocks
diff --git a/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml b/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml
index 7960352..cf63a1a 100644
--- a/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml
+++ b/base/RuoYi-Vue-master/ruoyi-admin/src/main/resources/application.yml
@@ -6,8 +6,9 @@ ruoyi:
version: 3.9.2
# 版权年份
copyrightYear: 2026
- # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
- profile: C:/Users/zfjia/work/projects/ruoyi/uploadPath
+ # 文件路径:按环境在 application-local.yml / application-prod.yml 中设置;
+ # 也可用环境变量 RUOYI_PROFILE 或 -Druoyi.profile= 覆盖(优先级更高)
+ profile: ${RUOYI_PROFILE:./uploadPath}
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
@@ -44,9 +45,10 @@ logging:
com.ruoyi.iot.lock.mapper: info
com.ruoyi.iot.person.mapper: info
file:
- # 日志输出目录(logback-spring.xml 通过 springProperty 读取;Linux 示例 /home/ruoyi/logs)
- # 也可用环境变量 LOGGING_FILE_PATH 覆盖,无需改配置文件
- path: C:/Users/zfjia/work/projects/ruoyi/logs
+ # 日志输出目录(logback-spring.xml 通过 springProperty 读取)
+ # 按环境在 application-local.yml / application-prod.yml 中设置;
+ # 也可用环境变量 LOGGING_FILE_PATH 或 -Dlogging.file.path= 覆盖(优先级更高)
+ path: ${LOGGING_FILE_PATH:logs}
# 用户配置
user:
@@ -63,7 +65,7 @@ spring:
# 国际化资源文件路径
basename: i18n/messages
profiles:
- active: druid
+ active: druid,prod
# 文件上传
servlet:
multipart:
@@ -157,81 +159,3 @@ xss:
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
-# IOT设备接入(MQTT)
-iot:
- # 门锁协议主题:ydlink/{id}/thing/...,换厂家或改前缀时只改这里
- lock:
- protocol:
- prefix: ydlink
- thing: thing
- # 人脸特征提取服务(数据库 lock_service_config 中 config_type=02 的配置优先级更高)
- face:
- endpoint: http://192.168.1.202:8802/bdws_api/Third/Api/Easen/getFaceFeatureData
- # 调用超时(毫秒)
- timeout: 10000
- # 第三方开放接口:关闭时接口返回 404;启用后调用方需带请求头 X-API-Key
- openapi:
- enabled: true
- # 应用清单以数据表 lock_open_app 为准,由「开放管理 › 应用接入」维护(改完即时生效,不用重启)。
- # 本段 api-key / apps 仅作两用:首次启动且表为空时原样导入;表不可用(未执行 D21 脚本)时降级使用。
- # 导入完成后可删除本段,只保留 enabled 与 path-prefix。
- # 单应用兼容配置:apps 为空时以此 Key 导入,appCode 记为 DEFAULT
- api-key: please-change-me
- path-prefix: /openapi/iot
- # 多应用配置(仅首次导入/降级用):每个业务系统一个独立 Key,
- # 命中哪个应用就以该应用的 app-code 作为数据隔离身份(写入 lock_stay.biz_system)。
- # 各应用 api-key 不得重复(重复或为空时启动即失败)。
- # callback-secret 用于回调签名 HMAC-SHA256(timestamp + "." + body),为空表示不开通回调。示例:
- # apps:
- # - app-code: PMS
- # app-name: 公寓 PMS
- # api-key: <每个应用独立>
- # callback-url: https://pms.example.com/hook/lock
- # callback-secret: <回调签名密钥>
- # enabled: true
- # - app-code: VISITOR
- # app-name: 访客系统
- # api-key: <另一个独立 Key>
- # callback-url: https://visitor.example.com/api/lock-callback
- # callback-secret: <回调签名密钥>
- # enabled: true
- # 服务器系统时间:开门授权依赖时间窗,内网离线部署时可在此开放管理端校时。
- # 高危操作:需 Linux + systemd(timedatectl),服务以 root 运行或配置免密 sudo。
- # control-enabled=false 时接口只读;非 root 运行且已为该用户配置 NOPASSWD 时把 use-sudo 设为 true。
- time:
- control-enabled: false
- use-sudo: false
- timeout-ms: 8000
- mqtt:
- # 开关,false 时不创建任何连接
- enabled: true
- # MQTT服务器地址,ssl 场景使用 ssl://host:8883
- broker-url: tcp://192.168.1.202:1883
- # 客户端ID,同一服务器下必须唯一;为空时自动生成
- client-id:
- username: admin5
- password: admin5
- # false 时服务端保留离线消息,需配合固定 client-id
- clean-session: true
- connection-timeout: 30
- keep-alive-interval: 60
- # 断线自动重连
- automatic-reconnect: true
- max-reconnect-delay: 30000
- # 连接健康检查周期(毫秒),0 表示不做定时重连
- health-check-interval: 30000
- # 发布消息等待服务端确认时间(毫秒),0 表示不等待
- publish-timeout: 3000
- # 消息持久化目录,为空使用内存
- persistence-directory:
- default-qos: 1
- default-retained: false
- # 通用静态订阅;门禁上行主题由 LockTopicSubscriber 按 iot.lock.protocol 前缀动态登记,
- # 这里不要再写一遍,否则改前缀时会出现两套主题并存
- topics: []
- # 遗嘱消息:无法区分主动断开与异常掉线,门禁场景建议关闭(离线由 GatewayOfflineCheckTask 判定)
- # will:
- # topic: ydlink/status
- # payload: offline
- # qos: 1
- # retained: true
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockCommandPolicy.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockCommandPolicy.java
index dbc91e6..39e3e8c 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockCommandPolicy.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockCommandPolicy.java
@@ -30,6 +30,15 @@ public final class LockCommandPolicy
/** 第 n 次重试的延迟(秒),超出后固定用最后一档 */
private static final int[] RETRY_DELAY_SECONDS = { 5, 15, 45, 120, 300 };
+ /** 网关忙后的重排间隔(秒):网关处理完当前 data/batch 需要时间,过短只会再次撞忙 */
+ private static final int BUSY_RETRY_SECONDS = 8;
+
+ /** 授权命令网关忙的重排次数上限,约 8 秒一轮;超过则认为链路异常,按失败终态放行后续命令 */
+ private static final int AUTH_MAX_BUSY_RETRY = 12;
+
+ /** 其它命令网关忙的重排次数上限 */
+ private static final int DEFAULT_MAX_BUSY_RETRY = 3;
+
static
{
TIMEOUT_SECONDS.put(LockConstants.CMD_BIND, 60);
@@ -45,6 +54,8 @@ public final class LockCommandPolicy
TIMEOUT_SECONDS.put(LockConstants.CMD_CLEAR_DEVICE, 60);
TIMEOUT_SECONDS.put(LockConstants.CMD_AUTH_PUSH, 90);
TIMEOUT_SECONDS.put(LockConstants.CMD_AUTH_CLEAR, 90);
+ // 查询只用于对账,首行返回即视为有响应;等不到响应时短超时避免长期占用网关
+ TIMEOUT_SECONDS.put(LockConstants.CMD_READ_AUTH, 30);
NO_RETRY.add(LockConstants.CMD_SYNC_TIME);
NO_RETRY.add(LockConstants.CMD_SWITCH);
@@ -105,15 +116,42 @@ public final class LockCommandPolicy
}
/**
- * 设备忙时的重试时间点:秒级重排,不占用普通退避档位
+ * 网关忙时的重试时间点:秒级重排,不占用普通退避档位
*/
public static Date busyRetryTime()
{
Calendar calendar = Calendar.getInstance();
- calendar.add(Calendar.SECOND, 3);
+ calendar.add(Calendar.SECOND, BUSY_RETRY_SECONDS);
return calendar.getTime();
}
+ /**
+ * 网关忙的重排次数上限
+ */
+ public static int maxBusyRetry(String commandType)
+ {
+ if (LockConstants.CMD_AUTH_PUSH.equals(commandType) || LockConstants.CMD_AUTH_CLEAR.equals(commandType))
+ {
+ return AUTH_MAX_BUSY_RETRY;
+ }
+ return DEFAULT_MAX_BUSY_RETRY;
+ }
+
+ /**
+ * 网关已回 10 之后,等待门锁终态的时长(秒)
+ *
+ * 10 只代表网关已接收并转发,门锁执行结果随后才会到;这段时间内既不能重发,
+ * 也不能沿用下发超时,否则 90 秒一到就被判失败、队列提前放行下一条。
+ */
+ public static int deviceWaitSeconds(String commandType)
+ {
+ if (LockConstants.CMD_AUTH_PUSH.equals(commandType) || LockConstants.CMD_AUTH_CLEAR.equals(commandType))
+ {
+ return 180;
+ }
+ return timeoutSeconds(commandType);
+ }
+
/**
* 第 retryCount 次重试的时间点
*/
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockConstants.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockConstants.java
index 202e20b..9c2429c 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockConstants.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/LockConstants.java
@@ -52,6 +52,9 @@ public class LockConstants
/** 命令类型:清除授权 */
public static final String CMD_AUTH_CLEAR = "0012";
+ /** 命令类型:读取锁内卡/密码授权列表(op=getkey,动作 readkey) */
+ public static final String CMD_READ_AUTH = "0019";
+
/** 命令状态:待发送 */
public static final String STATUS_PENDING = "0";
@@ -70,6 +73,12 @@ public class LockConstants
/** 命令状态:已取消 */
public static final String STATUS_CANCELED = "5";
+ /** 授权来源:平台维护(与 lock_stay.source 同编码) */
+ public static final String AUTH_SOURCE_PLATFORM = "1";
+
+ /** 授权来源:开放接口(直接授权与外部入住/换房,与 lock_stay.source 同编码) */
+ public static final String AUTH_SOURCE_OPENAPI = "2";
+
/** 授权类型:人脸 */
public static final String AUTH_FACE = "01";
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockAuthController.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockAuthController.java
index 5f196c6..6e50717 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockAuthController.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockAuthController.java
@@ -18,7 +18,10 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.iot.lock.LockConstants;
import com.ruoyi.iot.lock.domain.LockAuth;
+import com.ruoyi.iot.lock.domain.LockAuthBatchRequest;
+import com.ruoyi.iot.lock.domain.LockAuthPerson;
import com.ruoyi.iot.lock.service.ILockAuthService;
/**
@@ -37,11 +40,97 @@ public class LockAuthController extends BaseController
@GetMapping("/list")
public TableDataInfo list(LockAuth auth)
{
+ // 平台页签只统计平台授权,外部授权走 /external/list
+ auth.setSource(LockConstants.AUTH_SOURCE_PLATFORM);
startPage();
List list = lockAuthService.selectAuthList(auth);
return getDataTable(list);
}
+ /** 外部授权分页列表:一授权一行,固定 source='2' */
+ @PreAuthorize("@ss.hasPermi('lock:auth:list')")
+ @GetMapping("/external/list")
+ public TableDataInfo externalList(LockAuth query)
+ {
+ startPage();
+ List list = lockAuthService.selectExternalAuthList(query);
+ return getDataTable(list);
+ }
+
+ /** 全部授权分页列表:一授权一行,平台与外部授权都包含,用于逐条排查下发情况 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:list')")
+ @GetMapping("/all/list")
+ public TableDataInfo allList(LockAuth query)
+ {
+ startPage();
+ List list = lockAuthService.selectAllAuthList(query);
+ return getDataTable(list);
+ }
+
+ /** 外部授权详情:不返回授权凭据 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:query')")
+ @GetMapping("/external/{authId}")
+ public AjaxResult externalInfo(@PathVariable Long authId)
+ {
+ return success(lockAuthService.selectExternalAuthById(authId));
+ }
+
+ /** 修改外部授权有效期;有效授权保存后自动重推 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:edit')")
+ @Log(title = "外部授权修改", businessType = BusinessType.UPDATE)
+ @PutMapping("/external/{authId}")
+ public AjaxResult externalEdit(@PathVariable Long authId, @RequestBody LockAuth auth)
+ {
+ auth.setAuthId(authId);
+ auth.setUpdateBy(getUsername());
+ return toAjax(lockAuthService.updateExternalAuth(authId, auth));
+ }
+
+ @PreAuthorize("@ss.hasPermi('lock:auth:push')")
+ @Log(title = "外部授权下发", businessType = BusinessType.UPDATE)
+ @PostMapping("/external/{authId}/push")
+ public AjaxResult externalPush(@PathVariable Long authId)
+ {
+ if (lockAuthService.selectExternalAuthById(authId) == null)
+ {
+ return error("外部授权不存在,authId=" + authId);
+ }
+ lockAuthService.push(authId);
+ return success();
+ }
+
+ @PreAuthorize("@ss.hasPermi('lock:auth:clear')")
+ @Log(title = "外部授权取消", businessType = BusinessType.UPDATE)
+ @PostMapping("/external/{authId}/clear")
+ public AjaxResult externalClear(@PathVariable Long authId)
+ {
+ if (lockAuthService.selectExternalAuthById(authId) == null)
+ {
+ return error("外部授权不存在,authId=" + authId);
+ }
+ lockAuthService.clear(authId);
+ return success();
+ }
+
+ /** 删除外部授权:混入非外部授权整批拒绝 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:remove')")
+ @Log(title = "外部授权删除", businessType = BusinessType.DELETE)
+ @DeleteMapping("/external/{authIds}")
+ public AjaxResult externalRemove(@PathVariable Long[] authIds)
+ {
+ return toAjax(lockAuthService.deleteExternalAuthByIds(authIds));
+ }
+
+ /** 人员维度授权列表:一人一行,便于按人查看与维护其全部门锁授权 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:list')")
+ @GetMapping("/person/list")
+ public TableDataInfo personList(LockAuthPerson query)
+ {
+ startPage();
+ List list = lockAuthService.selectPersonAuthList(query);
+ return getDataTable(list);
+ }
+
@PreAuthorize("@ss.hasPermi('lock:auth:query')")
@GetMapping(value = "/{authId}")
public AjaxResult getInfo(@PathVariable Long authId)
@@ -63,6 +152,14 @@ public class LockAuthController extends BaseController
return toAjax(lockAuthService.insertAuth(auth));
}
+ @PreAuthorize("@ss.hasPermi('lock:auth:add')")
+ @Log(title = "门锁授权批量新增", businessType = BusinessType.INSERT)
+ @PostMapping("/batch")
+ public AjaxResult batchAdd(@RequestBody LockAuthBatchRequest request)
+ {
+ return success(lockAuthService.batchInsertAuth(request));
+ }
+
@PreAuthorize("@ss.hasPermi('lock:auth:edit')")
@Log(title = "门禁授权", businessType = BusinessType.UPDATE)
@PutMapping
@@ -79,6 +176,34 @@ public class LockAuthController extends BaseController
return toAjax(lockAuthService.deleteAuthByIds(authIds));
}
+ /** 仅删除授权记录:设备侧已清除的行可从平台移除,不再下发清除指令 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:remove')")
+ @Log(title = "授权记录删除", businessType = BusinessType.DELETE)
+ @DeleteMapping("/record/{authIds}")
+ public AjaxResult removeRecords(@PathVariable Long[] authIds)
+ {
+ return toAjax(lockAuthService.deleteAuthRecordsByIds(authIds));
+ }
+
+ /** 按人员删除:回收该人员名下所有门锁授权 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:remove')")
+ @Log(title = "按人员删除门禁授权", businessType = BusinessType.DELETE)
+ @PostMapping("/person/remove")
+ public AjaxResult removeByPerson(@RequestBody String[] personNos)
+ {
+ return toAjax(lockAuthService.deleteAuthsByPersonNos(personNos));
+ }
+
+ /** 按人员重新下发:下发其名下所有可下发的授权 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:push')")
+ @Log(title = "按人员下发授权", businessType = BusinessType.UPDATE)
+ @PostMapping("/person/push")
+ public AjaxResult pushByPerson(@RequestBody String[] personNos)
+ {
+ lockAuthService.batchPushByPersonNos(personNos);
+ return success();
+ }
+
@PreAuthorize("@ss.hasPermi('lock:auth:push')")
@Log(title = "授权下发", businessType = BusinessType.UPDATE)
@PostMapping("/{authId}/push")
@@ -120,6 +245,7 @@ public class LockAuthController extends BaseController
@PostMapping("/export")
public void export(HttpServletResponse response, LockAuth auth)
{
+ auth.setSource(LockConstants.AUTH_SOURCE_PLATFORM);
List list = lockAuthService.selectAuthList(auth);
ExcelUtil util = new ExcelUtil(LockAuth.class);
util.exportExcel(response, list, "门禁授权数据");
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockDeviceController.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockDeviceController.java
index 7ca48d3..dceb66f 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockDeviceController.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/controller/LockDeviceController.java
@@ -1,5 +1,6 @@
package com.ruoyi.iot.lock.controller;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletResponse;
@@ -20,8 +21,11 @@ import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.iot.lock.domain.LockAuth;
+import com.ruoyi.iot.lock.domain.LockCommand;
import com.ruoyi.iot.lock.domain.LockDevice;
+import com.ruoyi.iot.lock.domain.LockDeviceAuthClearRequest;
import com.ruoyi.iot.lock.service.ILockAuthService;
+import com.ruoyi.iot.lock.service.ILockDeviceAuthService;
import com.ruoyi.iot.lock.service.ILockDeviceService;
/**
@@ -39,6 +43,9 @@ public class LockDeviceController extends BaseController
@Autowired
private ILockAuthService lockAuthService;
+ @Autowired
+ private ILockDeviceAuthService lockDeviceAuthService;
+
@PreAuthorize("@ss.hasPermi('lock:device:list')")
@GetMapping("/list")
public TableDataInfo list(LockDevice device)
@@ -167,6 +174,47 @@ public class LockDeviceController extends BaseController
return getDataTable(list);
}
+ /** 读取锁内卡/密码授权(op=getkey),返回查询命令编号供页面轮询 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:list')")
+ @Log(title = "锁内授权读取", businessType = BusinessType.OTHER)
+ @PostMapping("/{deviceId}/auth/query")
+ public AjaxResult queryDeviceAuth(@PathVariable Long deviceId)
+ {
+ return success(commandSummary(lockDeviceAuthService.startQuery(deviceId)));
+ }
+
+ /** 锁内授权快照与平台授权对账结果 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:list')")
+ @GetMapping("/{deviceId}/auth/snapshot")
+ public AjaxResult deviceAuthSnapshot(@PathVariable Long deviceId)
+ {
+ return success(lockDeviceAuthService.selectSnapshot(deviceId));
+ }
+
+ /** 清除设备侧一条卡/密码授权 */
+ @PreAuthorize("@ss.hasPermi('lock:auth:clear')")
+ @Log(title = "锁内授权清除", businessType = BusinessType.UPDATE)
+ @PostMapping("/{deviceId}/auth/clear")
+ public AjaxResult clearDeviceAuth(@PathVariable Long deviceId, @RequestBody LockDeviceAuthClearRequest request)
+ {
+ if (request == null || request.getDeviceAuthId() == null)
+ {
+ return AjaxResult.error("缺少设备侧授权记录ID");
+ }
+ LockCommand command = lockDeviceAuthService.clearCredential(deviceId, request.getDeviceAuthId(),
+ request.getAuthType());
+ return success(commandSummary(command));
+ }
+
+ /** 命令摘要:不回传 payload,避免门锁密码与凭据泄露到页面 */
+ private Map commandSummary(LockCommand command)
+ {
+ Map data = new HashMap();
+ data.put("commandNo", command.getCommandNo());
+ data.put("commandStatus", command.getCommandStatus());
+ return data;
+ }
+
@PreAuthorize("@ss.hasPermi('lock:device:export')")
@Log(title = "智能门锁", businessType = BusinessType.EXPORT)
@PostMapping("/export")
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAccessLog.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAccessLog.java
index 658264c..8bdd6d8 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAccessLog.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAccessLog.java
@@ -30,6 +30,10 @@ public class LockAccessLog implements Serializable
@Excel(name = "门锁SN")
private String deviceSn;
+ /** 房间号(按门锁当前绑定关系关联,未绑定房间时为空) */
+ @Excel(name = "房间号")
+ private String roomNo;
+
/** 上报网关ID */
private Long gatewayId;
@@ -68,8 +72,9 @@ public class LockAccessLog implements Serializable
@Excel(name = "开门时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date openTime;
- /** 服务端接收时间 */
+ /** 服务端接收时间(数据上报时间) */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ @Excel(name = "上报时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date recvTime;
/** 报文序号(幂等去重) */
@@ -117,6 +122,16 @@ public class LockAccessLog implements Serializable
this.deviceSn = deviceSn;
}
+ public String getRoomNo()
+ {
+ return roomNo;
+ }
+
+ public void setRoomNo(String roomNo)
+ {
+ this.roomNo = roomNo;
+ }
+
public Long getGatewayId()
{
return gatewayId;
@@ -288,7 +303,8 @@ public class LockAccessLog implements Serializable
public String toString()
{
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE).append("logId", getLogId())
- .append("deviceSn", getDeviceSn()).append("personNo", getPersonNo()).append("cardNo", getCardNo())
+ .append("deviceSn", getDeviceSn()).append("roomNo", getRoomNo())
+ .append("personNo", getPersonNo()).append("cardNo", getCardNo())
.append("openType", getOpenType()).append("openResult", getOpenResult())
.append("openTime", getOpenTime()).append("recvTime", getRecvTime()).append("seq", getSeq())
.toString();
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAuth.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAuth.java
index c68b67a..aef38ad 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAuth.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/domain/LockAuth.java
@@ -66,6 +66,30 @@ public class LockAuth extends BaseEntity
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date authTime;
+ /** 授权来源(1平台 2开放接口) */
+ private String source;
+
+ /** 来源应用编码(source=2 时对应 lock_open_app.app_code) */
+ private String sourceAppCode;
+
+ /** 来源业务单号(外部入住/换房为 biz_no,直接授权可空) */
+ private String sourceBizNo;
+
+ /** 展示:来源应用名称(应用已删除时为空,页面退回显示编码) */
+ private String sourceAppName;
+
+ /** 展示:关联入住单号(外部入住/换房授权) */
+ private String stayNo;
+
+ /** 展示:门锁名称 */
+ private String deviceName;
+
+ /** 展示:门锁在线状态(0在线 1离线 2未知) */
+ private String onlineStatus;
+
+ /** 查询:授权编号 / 人员编号 / 人员姓名模糊匹配(外部授权列表) */
+ private String keyword;
+
/** 下发同步状态(0待下发 1已下发 2下发成功 3下发失败 4已清除) */
@Excel(name = "同步状态", readConverterExp = "0=待下发,1=已下发,2=下发成功,3=下发失败,4=已清除")
private String syncStatus;
@@ -226,6 +250,86 @@ public class LockAuth extends BaseEntity
this.authTime = authTime;
}
+ public String getSource()
+ {
+ return source;
+ }
+
+ public void setSource(String source)
+ {
+ this.source = source;
+ }
+
+ public String getSourceAppCode()
+ {
+ return sourceAppCode;
+ }
+
+ public void setSourceAppCode(String sourceAppCode)
+ {
+ this.sourceAppCode = sourceAppCode;
+ }
+
+ public String getSourceBizNo()
+ {
+ return sourceBizNo;
+ }
+
+ public void setSourceBizNo(String sourceBizNo)
+ {
+ this.sourceBizNo = sourceBizNo;
+ }
+
+ public String getSourceAppName()
+ {
+ return sourceAppName;
+ }
+
+ public void setSourceAppName(String sourceAppName)
+ {
+ this.sourceAppName = sourceAppName;
+ }
+
+ public String getStayNo()
+ {
+ return stayNo;
+ }
+
+ public void setStayNo(String stayNo)
+ {
+ this.stayNo = stayNo;
+ }
+
+ public String getDeviceName()
+ {
+ return deviceName;
+ }
+
+ public void setDeviceName(String deviceName)
+ {
+ this.deviceName = deviceName;
+ }
+
+ public String getOnlineStatus()
+ {
+ return onlineStatus;
+ }
+
+ public void setOnlineStatus(String onlineStatus)
+ {
+ this.onlineStatus = onlineStatus;
+ }
+
+ public String getKeyword()
+ {
+ return keyword;
+ }
+
+ public void setKeyword(String keyword)
+ {
+ this.keyword = keyword;
+ }
+
public String getSyncStatus()
{
return syncStatus;
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockAuthMapper.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockAuthMapper.java
index 7c93e8c..92e7a00 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockAuthMapper.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockAuthMapper.java
@@ -3,6 +3,7 @@ package com.ruoyi.iot.lock.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.iot.lock.domain.LockAuth;
+import com.ruoyi.iot.lock.domain.LockAuthPerson;
/**
* 授权数据 数据层
@@ -13,8 +14,35 @@ public interface LockAuthMapper
{
List selectAuthList(LockAuth auth);
+ /** 按人员聚合授权:一人一行,含授权总数、已下发数、失败数与涉及门锁数 */
+ List selectPersonAuthList(LockAuthPerson query);
+
+ /** 人员的全部未删除授权ID(含已清除行),按人员删除时使用 */
+ List selectAuthIdsByPersonNos(@Param("personNos") String[] personNos);
+
+ /** 人员可重新下发的授权ID:排除已清除(4)与待清除(6),避免把删除中的人脸重新 add */
+ List selectPushableAuthIdsByPersonNos(@Param("personNos") String[] personNos);
+
LockAuth selectAuthById(Long authId);
+ /** 外部授权分页列表:固定 source='2',带来源应用、门锁与入住单展示字段 */
+ List selectExternalAuthList(LockAuth query);
+
+ /** 全部授权分页列表:一授权一行,不区分来源,带来源应用、门锁与入住单展示字段 */
+ List selectAllAuthList(LockAuth query);
+
+ /** 外部授权详情(不含 auth_value) */
+ LockAuth selectExternalAuthById(Long authId);
+
+ /** 按应用校验外部授权归属;不属于该应用时返回 null */
+ LockAuth selectExternalAuthForApp(@Param("authId") Long authId, @Param("appCode") String appCode);
+
+ /** 接管平台授权归属(仅外部入住/换房使用) */
+ int updateAuthSource(LockAuth auth);
+
+ /** 外部授权改期:允许把 end_time 更新为 NULL */
+ int updateExternalAuth(LockAuth auth);
+
List selectAuthsByDeviceId(Long deviceId);
List selectAuthsByPersonNo(String personNo);
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockCommandMapper.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockCommandMapper.java
index f8d73fa..bc3a927 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockCommandMapper.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockCommandMapper.java
@@ -24,6 +24,30 @@ public interface LockCommandMapper
/** 指定分组仍在途(待发送/已发送)的授权类命令数,用于同一网关串行下发 */
int countInflightAuthCommands(@Param("groupId") Long groupId);
+ /** 同一主题(同网关)除指定命令外仍在途(待发送/已发送)的命令数,用于普通失败重发前串行校验 */
+ int countOtherInflightByTopic(@Param("topic") String topic, @Param("commandId") Long commandId);
+
+ /** 同一主题除指定命令外已发送的命令数,用于忙排队命令重发前串行校验 */
+ int countOtherSentByTopic(@Param("topic") String topic, @Param("commandId") Long commandId);
+
+ /** 到期待重发的排队命令(网关忙),按命令ID升序保证先来先发 */
+ List selectDeferredCommands(Date now);
+
+ /** 指定授权是否已有在途命令,避免补偿与命令重试重复下发 */
+ int countInflightAuthByBizId(@Param("bizId") Long bizId);
+
+ /** 指定业务对象是否已有指定类型的在途命令(锁内授权查询按门锁去重) */
+ int countInflightByBizId(@Param("commandType") String commandType, @Param("bizId") Long bizId);
+
+ /** 指定业务对象最近一次指定类型的命令 */
+ LockCommand selectLatestByBizId(@Param("commandType") String commandType, @Param("bizId") Long bizId);
+
+ /** 指定业务对象最近一次有返回行的查询命令(最近一次失败时回退展示) */
+ LockCommand selectLatestWithRowsByBizId(@Param("commandType") String commandType, @Param("bizId") Long bizId);
+
+ /** 批量按命令编号取命令,用于对账列表一次性判定清除状态 */
+ List selectCommandsByNos(@Param("commandNos") List commandNos);
+
int insertCommand(LockCommand command);
int updateCommand(LockCommand command);
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockGatewayMapper.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockGatewayMapper.java
index 3902be7..1a7bba5 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockGatewayMapper.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mapper/LockGatewayMapper.java
@@ -27,6 +27,9 @@ public interface LockGatewayMapper
int updateOnlineStatus(@Param("gatewayId") Long gatewayId, @Param("onlineStatus") String onlineStatus,
@Param("lastOnlineTime") Date lastOnlineTime);
+ /** 只改在线状态,保留 last_online_time(离线通知使用) */
+ int updateOnlineStatusOnly(@Param("gatewayId") Long gatewayId, @Param("onlineStatus") String onlineStatus);
+
int updateSyncStatus(@Param("gatewayId") Long gatewayId, @Param("syncStatus") String syncStatus,
@Param("syncMsg") String syncMsg);
@@ -37,4 +40,11 @@ public interface LockGatewayMapper
/** 超过指定时间未上报的网关置为离线 */
int markOfflineBefore(@Param("offlineTime") Date offlineTime, @Param("now") Date now);
+
+ /**
+ * 按归属行的最近上报时间把网关置为在线
+ *
+ * 网关能转发门锁信号/记录即证明其在线,不再只依赖 onoff_line 连接事件。
+ */
+ int refreshOnlineByReport(@Param("onlineTime") Date onlineTime);
}
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/LockMessageListener.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/LockMessageListener.java
index 4d842b7..8ec27b3 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/LockMessageListener.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/LockMessageListener.java
@@ -18,6 +18,7 @@ import com.ruoyi.iot.lock.protocol.YdlinkOp;
import com.ruoyi.iot.lock.protocol.YdlinkPayloadMasker;
import com.ruoyi.iot.lock.protocol.YdlinkTopicBuilder;
import com.ruoyi.iot.lock.protocol.YdlinkTopicDTO;
+import com.ruoyi.iot.lock.service.ILockDeviceAuthService;
import com.ruoyi.iot.lock.service.ILockMqttLogService;
import com.ruoyi.iot.domain.MqttMessageDTO;
@@ -46,6 +47,9 @@ public class LockMessageListener implements IMqttMessageListener
@Autowired
private LockSignalHandler lockSignalHandler;
+ @Autowired
+ private ILockDeviceAuthService deviceAuthService;
+
@Autowired
private ILockMqttLogService mqttLogService;
@@ -99,7 +103,15 @@ public class LockMessageListener implements IMqttMessageListener
result = commandRespHandler.handle(topic, frame, message.getPayload());
break;
case GROUP_DATA_BATCH:
- result = commandRespHandler.handle(topic, frame, message.getPayload());
+ if (YdlinkOp.GET_KEY.equals(frame.getOp()))
+ {
+ // getkey 应答不是「动作+结果码」形态,第 2 位是总数,不能交给通用结果码解析
+ result = handleKeyReport(topic, frame, message.getPayload());
+ }
+ else
+ {
+ result = commandRespHandler.handle(topic, frame, message.getPayload());
+ }
break;
case GROUP_DATA_BATCH_REPORT:
if (YdlinkOp.LOCK_RSSI.equals(frame.getOp()))
@@ -108,6 +120,10 @@ public class LockMessageListener implements IMqttMessageListener
int updated = lockSignalHandler.handle(topic, frame);
result = "信号电量更新 " + updated + " 台";
}
+ else if (YdlinkOp.GET_KEY.equals(frame.getOp()))
+ {
+ result = handleKeyReport(topic, frame, message.getPayload());
+ }
else
{
result = commandRespHandler.handle(topic, frame, message.getPayload());
@@ -140,6 +156,12 @@ public class LockMessageListener implements IMqttMessageListener
recordLog(topic, frame, message, status, result, cost(start));
}
+ private String handleKeyReport(YdlinkTopicDTO topic, YdlinkFrameDTO frame, String payload)
+ {
+ return deviceAuthService.handleQueryReport(topic.getTopic(), frame.getSeq(),
+ YdlinkCodec.toRecordList(frame.getParams()), payload);
+ }
+
private void recordLog(YdlinkTopicDTO topic, YdlinkFrameDTO frame, MqttMessageDTO message, String handleStatus,
String handleMsg, int costTime)
{
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/CommandRespHandler.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/CommandRespHandler.java
index 3818650..f7cd2f7 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/CommandRespHandler.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/CommandRespHandler.java
@@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.iot.lock.LockCommandPolicy;
import com.ruoyi.iot.lock.LockConstants;
import com.ruoyi.iot.lock.protocol.YdlinkCodec;
import com.ruoyi.iot.lock.protocol.YdlinkFrameDTO;
@@ -26,9 +27,10 @@ import com.ruoyi.iot.lock.service.ILockCommandService;
* 覆盖:property/set、action/execute、data/batch(设备在请求主题反向回 ac=resp),
* 以及协议登记的 *_response、data/batch_report 主题。
*
- * 门锁类下发可能收到两次响应:
+ * 门锁类下发可能收到多次响应:
* 1) 网关回执(结果码 10):仅表示消息已送达网关并转发,属中间态;
- * 2) 门锁回执(结果码 0/11/12):终态,据此判定成功与失败。
+ * 2) 网关忙(结果码 11):报文未转发到门锁,属可重发的瞬时状态,不能判失败;
+ * 3) 门锁回执(结果码 0/12):终态,0 成功、12 设备不存在,据此判定结果。
* 结果码位置按 op 由 {@link YdlinkRespSpec} 决定,不再按首项是否为动作名猜测。
*
* @author ruoyi
@@ -64,9 +66,9 @@ public class CommandRespHandler
return "应答缺少可识别结果码,命令保持等待";
}
String message = parseMessage(first);
- if (YdlinkRespSpec.CODE_DEVICE_BUSY.equals(resp.getCode()))
+ if (YdlinkRespSpec.CODE_DEVICE_BUSY.equals(resp.getCode()) && !LockCommandPolicy.isBusy(message))
{
- // 结果码 11 才是「设备忙」的权威信号,设备不一定附带人读文本;
+ // 结果码 11 才是「网关忙」的权威信号,设备不一定附带人读文本;
// 补上标记后,命令层才能按秒级重排而不是走普通失败退避
message = StringUtils.isEmpty(message) ? "gw busy" : message + " gw busy";
}
@@ -77,6 +79,21 @@ public class CommandRespHandler
}
boolean gatewayAck = resp.getStage() == YdlinkRespSpec.Stage.GATEWAY_ACK;
boolean success = resp.getStage() == YdlinkRespSpec.Stage.SUCCESS;
+ if (resp.getStage() == YdlinkRespSpec.Stage.GATEWAY_BUSY)
+ {
+ // 网关忙表示报文没有转发到门锁,不能按门锁失败处理:
+ // 命令置为待发送并按秒级排队重发,同网关后续命令继续等待,不再立即下发下一条
+ String finalStatus = commandService.handleGatewayBusy(topic.getTopic(), seq, payload, message);
+ if (finalStatus == null)
+ {
+ log.info("网关忙,命令已排队重发, topic={}, seq={}, op={}", topic.getTopic(), seq, frame.getOp());
+ return "网关忙,已排队重发";
+ }
+ authService.handleResult(seq, LockConstants.STATUS_SUCCESS.equals(finalStatus), message);
+ log.info("网关持续忙,命令按失败终结, topic={}, seq={}, op={}, status={}", topic.getTopic(), seq,
+ frame.getOp(), finalStatus);
+ return "执行失败:" + message;
+ }
String finalStatus = gatewayAck
? commandService.handleGatewayAck(topic.getTopic(), seq, payload, message)
: commandService.handleResponse(topic.getTopic(), seq, payload, success, message);
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/GatewayOnlineHandler.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/GatewayOnlineHandler.java
index 8190275..93dfb86 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/GatewayOnlineHandler.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/mqtt/handler/GatewayOnlineHandler.java
@@ -1,5 +1,6 @@
package com.ruoyi.iot.lock.mqtt.handler;
+import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -12,6 +13,10 @@ import com.ruoyi.iot.lock.protocol.YdlinkTopicDTO;
/**
* 网关上下线处理:ydlink/{gatewaySn}/thing/onoff_line
+ *
+ * 报文体没有标准信封,只有 devId 与 message:上线为 "I am back",离线为遗嘱消息
+ * "I will be back"。文案不作为唯一依据(固件改文案即失效),只在命中离线文案时置离线,
+ * 其余一律按在线处理并记录原文,便于现场核对。
*
* @author ruoyi
*/
@@ -20,6 +25,9 @@ public class GatewayOnlineHandler
{
private static final Logger log = LoggerFactory.getLogger(GatewayOnlineHandler.class);
+ /** 离线遗嘱文案关键字 */
+ private static final String OFFLINE_KEYWORD = "will be back";
+
@Autowired
private ILockGatewayService gatewayService;
@@ -37,12 +45,19 @@ public class GatewayOnlineHandler
{
return "非网关级主题";
}
- // 协议中上下线语义待确认:默认按“收到上报即在线”处理
- String onlineStatus = LockConstants.ONLINE;
- gatewayService.updateOnlineStatus(gatewaySn, onlineStatus);
LockGateway gateway = gatewayService.selectGatewayBySn(gatewaySn);
- log.info("网关状态更新, gatewaySn={}, onlineStatus={}, registered={}", gatewaySn, onlineStatus,
- gateway != null);
- return gateway == null ? "网关未登记,已忽略" : "已更新为在线";
+ String message = frame == null ? null : frame.getMessage();
+ if (gateway == null)
+ {
+ // 主题第2段与 lock_gateway.gateway_sn 对不上时不会更新任何行,这里必须留痕
+ log.warn("网关上下线报文未匹配到登记网关, gatewaySn={}, devId={}, message={}", gatewaySn,
+ frame == null ? null : frame.getDevId(), message);
+ return "网关未登记,已忽略";
+ }
+ boolean offline = message != null && message.toLowerCase(Locale.ROOT).contains(OFFLINE_KEYWORD);
+ String onlineStatus = offline ? LockConstants.OFFLINE : LockConstants.ONLINE;
+ gatewayService.updateOnlineStatus(gatewaySn, onlineStatus);
+ log.info("网关状态更新, gatewaySn={}, onlineStatus={}, message={}", gatewaySn, onlineStatus, message);
+ return offline ? "已更新为离线" : "已更新为在线";
}
}
diff --git a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/protocol/YdlinkFrameDTO.java b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/protocol/YdlinkFrameDTO.java
index aa319a7..898c7d9 100644
--- a/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/protocol/YdlinkFrameDTO.java
+++ b/base/RuoYi-Vue-master/ruoyi-iot/src/main/java/com/ruoyi/iot/lock/protocol/YdlinkFrameDTO.java
@@ -66,6 +66,14 @@ public class YdlinkFrameDTO implements Serializable
@JSONField(ordinal = 10)
private String chksum;
+ /**
+ * 状态文案(仅 onoff_line 使用:"I am back" 上线 / "I will be back" 离线)
+ *
+ * 该报文没有标准信封,只有 devId + message;只接收不回填。
+ */
+ @JSONField(serialize = false)
+ private String message;
+
/** 参数数组(一维或二维) */
@JSONField(ordinal = 11)
private List