1112
This commit is contained in:
1 parent
6e529c7ef7
commit
eb5ad9518d
36 files changed
+4195
-581
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
+40
-2
@@ -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 之后,等待门锁终态的时长(秒)
|
||||
* <p>
|
||||
* 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 次重试的时间点
|
||||
*/
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+126
@@ -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<LockAuth> 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<LockAuth> list = lockAuthService.selectExternalAuthList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/** 全部授权分页列表:一授权一行,平台与外部授权都包含,用于逐条排查下发情况 */
|
||||
@PreAuthorize("@ss.hasPermi('lock:auth:list')")
|
||||
@GetMapping("/all/list")
|
||||
public TableDataInfo allList(LockAuth query)
|
||||
{
|
||||
startPage();
|
||||
List<LockAuth> 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<LockAuthPerson> 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<LockAuth> list = lockAuthService.selectAuthList(auth);
|
||||
ExcelUtil<LockAuth> util = new ExcelUtil<LockAuth>(LockAuth.class);
|
||||
util.exportExcel(response, list, "门禁授权数据");
|
||||
|
||||
+48
@@ -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<String, Object> commandSummary(LockCommand command)
|
||||
{
|
||||
Map<String, Object> data = new HashMap<String, Object>();
|
||||
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")
|
||||
|
||||
+18
-2
@@ -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();
|
||||
|
||||
+104
@@ -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;
|
||||
|
||||
+28
@@ -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<LockAuth> selectAuthList(LockAuth auth);
|
||||
|
||||
/** 按人员聚合授权:一人一行,含授权总数、已下发数、失败数与涉及门锁数 */
|
||||
List<LockAuthPerson> selectPersonAuthList(LockAuthPerson query);
|
||||
|
||||
/** 人员的全部未删除授权ID(含已清除行),按人员删除时使用 */
|
||||
List<Long> selectAuthIdsByPersonNos(@Param("personNos") String[] personNos);
|
||||
|
||||
/** 人员可重新下发的授权ID:排除已清除(4)与待清除(6),避免把删除中的人脸重新 add */
|
||||
List<Long> selectPushableAuthIdsByPersonNos(@Param("personNos") String[] personNos);
|
||||
|
||||
LockAuth selectAuthById(Long authId);
|
||||
|
||||
/** 外部授权分页列表:固定 source='2',带来源应用、门锁与入住单展示字段 */
|
||||
List<LockAuth> selectExternalAuthList(LockAuth query);
|
||||
|
||||
/** 全部授权分页列表:一授权一行,不区分来源,带来源应用、门锁与入住单展示字段 */
|
||||
List<LockAuth> 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<LockAuth> selectAuthsByDeviceId(Long deviceId);
|
||||
|
||||
List<LockAuth> selectAuthsByPersonNo(String personNo);
|
||||
|
||||
+24
@@ -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<LockCommand> 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<LockCommand> selectCommandsByNos(@Param("commandNos") List<String> commandNos);
|
||||
|
||||
int insertCommand(LockCommand command);
|
||||
|
||||
int updateCommand(LockCommand command);
|
||||
|
||||
+10
@@ -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);
|
||||
|
||||
/**
|
||||
* 按归属行的最近上报时间把网关置为在线
|
||||
* <p>
|
||||
* 网关能转发门锁信号/记录即证明其在线,不再只依赖 onoff_line 连接事件。
|
||||
*/
|
||||
int refreshOnlineByReport(@Param("onlineTime") Date onlineTime);
|
||||
}
|
||||
+23
-1
@@ -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)
|
||||
{
|
||||
|
||||
+21
-4
@@ -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 主题。
|
||||
* <p>
|
||||
* 门锁类下发可能收到两次响应:<br>
|
||||
* 门锁类下发可能收到多次响应:<br>
|
||||
* 1) 网关回执(结果码 10):仅表示消息已送达网关并转发,属中间态;<br>
|
||||
* 2) 门锁回执(结果码 0/11/12):终态,据此判定成功与失败。<br>
|
||||
* 2) 网关忙(结果码 11):报文未转发到门锁,属可重发的瞬时状态,不能判失败;<br>
|
||||
* 3) 门锁回执(结果码 0/12):终态,0 成功、12 设备不存在,据此判定结果。<br>
|
||||
* 结果码位置按 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);
|
||||
|
||||
+21
-6
@@ -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
|
||||
* <p>
|
||||
* 报文体没有标准信封,只有 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 ? "已更新为离线" : "已更新为在线";
|
||||
}
|
||||
}
|
||||
+19
@@ -66,6 +66,14 @@ public class YdlinkFrameDTO implements Serializable
|
||||
@JSONField(ordinal = 10)
|
||||
private String chksum;
|
||||
|
||||
/**
|
||||
* 状态文案(仅 onoff_line 使用:"I am back" 上线 / "I will be back" 离线)
|
||||
* <p>
|
||||
* 该报文没有标准信封,只有 devId + message;只接收不回填。
|
||||
*/
|
||||
@JSONField(serialize = false)
|
||||
private String message;
|
||||
|
||||
/** 参数数组(一维或二维) */
|
||||
@JSONField(ordinal = 11)
|
||||
private List<Object> params;
|
||||
@@ -249,4 +257,15 @@ public class YdlinkFrameDTO implements Serializable
|
||||
this.params = params;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
public YdlinkFrameDTO setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+2
@@ -43,6 +43,8 @@ public final class YdlinkPayloadMasker
|
||||
SECRET_INDEXES.put(YdlinkOp.IC_CARD_MIX, new int[] { 2, 4 });
|
||||
// 配置服务器:账号 [6]、密码 [7]
|
||||
SECRET_INDEXES.put(YdlinkOp.CONFIG, new int[] { 6, 7 });
|
||||
// getkey:请求 [3]门锁密码;应答 [3]卡号/密码
|
||||
SECRET_INDEXES.put(YdlinkOp.GET_KEY, new int[] { 3 });
|
||||
}
|
||||
|
||||
private YdlinkPayloadMasker()
|
||||
|
||||
+16
-5
@@ -15,7 +15,8 @@ import java.util.Map;
|
||||
* → [1] 错误码、[5] 操作结果,两处都可能出现 10/0
|
||||
* </pre>
|
||||
* 因此按 op 维护取码位置,opencloselock 同时检查 [1] 与 [5]:
|
||||
* 任一为 10 视为网关已接收(中间态),出现 11/12 视为失败,其余以 0 判定成功。
|
||||
* 任一为 10 视为网关已接收(中间态),11 视为网关忙(报文未转发,可稍后重发),
|
||||
* 12 视为设备不存在(终态失败),其余以 0 判定成功。
|
||||
* <p>
|
||||
* locklist 的 add/clear 回执都以「动作名 + 结果码」两段返回(实测
|
||||
* {@code params:["clear","0"]}、{@code params:["add","0"]}),结果码位置一致。
|
||||
@@ -30,10 +31,10 @@ public final class YdlinkRespSpec
|
||||
/** 网关已接收并转发(中间态) */
|
||||
public static final String CODE_GATEWAY_ACK = "10";
|
||||
|
||||
/** 设备正忙 */
|
||||
/** 网关忙:报文未转发到门锁,属可重发的瞬时状态,不能当门锁终态失败 */
|
||||
public static final String CODE_DEVICE_BUSY = "11";
|
||||
|
||||
/** 设备不存在 */
|
||||
/** 设备不存在(终态失败) */
|
||||
public static final String CODE_DEVICE_NOT_FOUND = "12";
|
||||
|
||||
/** 应答阶段 */
|
||||
@@ -42,6 +43,9 @@ public final class YdlinkRespSpec
|
||||
/** 网关中间态,命令尚未完成 */
|
||||
GATEWAY_ACK,
|
||||
|
||||
/** 网关忙,报文未转发,稍后可重发 */
|
||||
GATEWAY_BUSY,
|
||||
|
||||
/** 设备终态:成功 */
|
||||
SUCCESS,
|
||||
|
||||
@@ -72,7 +76,8 @@ public final class YdlinkRespSpec
|
||||
// 查询类
|
||||
RESULT_INDEX.put(YdlinkOp.GET_SLAVE_INFO, new int[] { 2 });
|
||||
RESULT_INDEX.put(YdlinkOp.GET_RECORD, new int[] { 2 });
|
||||
RESULT_INDEX.put(YdlinkOp.GET_KEY, new int[] { 2 });
|
||||
// getkey 应答第 2 位是「总数」而非结果码,由 LockDeviceAuthServiceImpl 专解析;
|
||||
// 这里不登记,避免总数为 0 时被误判成功、非 0 时永远等待
|
||||
}
|
||||
|
||||
private YdlinkRespSpec()
|
||||
@@ -117,7 +122,13 @@ public final class YdlinkRespSpec
|
||||
{
|
||||
return new RespCode(code, Stage.GATEWAY_ACK);
|
||||
}
|
||||
if (CODE_DEVICE_BUSY.equals(code) || CODE_DEVICE_NOT_FOUND.equals(code))
|
||||
if (CODE_DEVICE_BUSY.equals(code))
|
||||
{
|
||||
// 网关同一时刻只处理一条 data/batch,11 时命令并未下发到门锁,
|
||||
// 必须保留重发机会;实测随后同一 seq 仍会回 0,按失败终结会丢掉成功回执
|
||||
return new RespCode(code, Stage.GATEWAY_BUSY);
|
||||
}
|
||||
if (CODE_DEVICE_NOT_FOUND.equals(code))
|
||||
{
|
||||
failCode = code;
|
||||
}
|
||||
|
||||
+51
@@ -2,6 +2,9 @@ package com.ruoyi.iot.lock.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.iot.lock.domain.LockAuth;
|
||||
import com.ruoyi.iot.lock.domain.LockAuthBatchRequest;
|
||||
import com.ruoyi.iot.lock.domain.LockAuthBatchResult;
|
||||
import com.ruoyi.iot.lock.domain.LockAuthPerson;
|
||||
|
||||
/**
|
||||
* 授权服务
|
||||
@@ -12,19 +15,67 @@ public interface ILockAuthService
|
||||
{
|
||||
List<LockAuth> selectAuthList(LockAuth auth);
|
||||
|
||||
/** 人员维度授权汇总:一人一行,含授权总数、已下发数、失败数与涉及门锁数 */
|
||||
List<LockAuthPerson> selectPersonAuthList(LockAuthPerson query);
|
||||
|
||||
LockAuth selectAuthById(Long authId);
|
||||
|
||||
/** 外部授权分页列表:固定 source='2',带来源应用、门锁与入住单展示字段 */
|
||||
List<LockAuth> selectExternalAuthList(LockAuth query);
|
||||
|
||||
/** 全部授权分页列表:一授权一行,不区分来源,便于逐条排查下发情况 */
|
||||
List<LockAuth> selectAllAuthList(LockAuth query);
|
||||
|
||||
/** 外部授权详情(不含 auth_value) */
|
||||
LockAuth selectExternalAuthById(Long authId);
|
||||
|
||||
/** 按应用校验外部授权归属;不属于该应用时返回 null */
|
||||
LockAuth selectExternalAuthForApp(Long authId, String appCode);
|
||||
|
||||
/** 修改外部授权有效期:已取消只保存,其余保存后重推 */
|
||||
int updateExternalAuth(Long authId, LockAuth changes);
|
||||
|
||||
/** 删除外部授权:先清除设备侧权限再逻辑删除;混入非外部授权整批拒绝 */
|
||||
int deleteExternalAuthByIds(Long[] authIds);
|
||||
|
||||
List<LockAuth> selectAuthsByDeviceId(Long deviceId);
|
||||
|
||||
/** 新增授权并下发 */
|
||||
int insertAuth(LockAuth auth);
|
||||
|
||||
/**
|
||||
* 新增授权并下发。
|
||||
*
|
||||
* @param adoptPlatformSource true 表示覆盖平台已有授权时接管归属(外部入住/换房使用);
|
||||
* false 表示拒绝覆盖平台授权(外部直接授权接口使用)
|
||||
*/
|
||||
int insertAuth(LockAuth auth, boolean adoptPlatformSource);
|
||||
|
||||
/**
|
||||
* 多锁批量授权:同一人员、同一类型、同一有效期,为多把门锁各建一条授权。
|
||||
* <p>
|
||||
* 每行独立处理与下发,单锁失败不影响其它锁;已存在的同键授权按覆盖处理。
|
||||
*
|
||||
* @param request 授权模板与门锁ID集合
|
||||
* @return 逐锁结果与汇总计数
|
||||
*/
|
||||
LockAuthBatchResult batchInsertAuth(LockAuthBatchRequest request);
|
||||
|
||||
/** 修改授权并重新下发 */
|
||||
int updateAuth(LockAuth auth);
|
||||
|
||||
/** 删除授权并下发清除指令 */
|
||||
int deleteAuthByIds(Long[] authIds);
|
||||
|
||||
/** 仅删除授权记录(逻辑删除),不向门锁下发清除;只允许设备侧已清除(4)的记录 */
|
||||
int deleteAuthRecordsByIds(Long[] authIds);
|
||||
|
||||
/** 删除人员名下全部授权:逐条下发清除后逻辑删除,返回删除行数 */
|
||||
int deleteAuthsByPersonNos(String[] personNos);
|
||||
|
||||
/** 按人员批量重新下发其名下可下发授权(跳过已清除与待清除) */
|
||||
void batchPushByPersonNos(String[] personNos);
|
||||
|
||||
/** 手动重新下发 */
|
||||
void push(Long authId);
|
||||
|
||||
|
||||
+12
-2
@@ -23,8 +23,8 @@ public interface ILockCommandService
|
||||
/**
|
||||
* 处理门锁回执(最终结果)
|
||||
* <p>
|
||||
* 门锁回执(0/11/12)即终态,直接判定成功或失败;
|
||||
* 网关回执(10)只是中间态,后到也不会推翻门锁结果。
|
||||
* 门锁回执(0/12)即终态,直接判定成功或失败;
|
||||
* 网关回执(10)只是中间态,后到也不会推翻门锁结果;网关忙(11)见 {@link #handleGatewayBusy}。
|
||||
*
|
||||
* @param topic 响应主题
|
||||
* @param seq 报文序列号
|
||||
@@ -46,6 +46,16 @@ public interface ILockCommandService
|
||||
*/
|
||||
String handleGatewayAck(String topic, String seq, String payload, String message);
|
||||
|
||||
/**
|
||||
* 处理网关忙(结果码 11,报文未转发到门锁)
|
||||
* <p>
|
||||
* 命令回到待发送并排期重发,期间保持「在途」以阻止同网关下发下一条;
|
||||
* 重排次数用尽才按失败终结。
|
||||
*
|
||||
* @return 重排中返回 null;按失败终结时返回 STATUS_FAIL
|
||||
*/
|
||||
String handleGatewayBusy(String topic, String seq, String payload, String message);
|
||||
|
||||
/**
|
||||
* 标记超时命令:候选按最短超时粗筛,再按各命令类型的超时阈值逐条判定
|
||||
*/
|
||||
|
||||
+422
-9
@@ -1,8 +1,10 @@
|
||||
package com.ruoyi.iot.lock.service.impl;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -14,6 +16,9 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.iot.lock.LockCommandPolicy;
|
||||
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.LockAuthBatchResult;
|
||||
import com.ruoyi.iot.lock.domain.LockAuthPerson;
|
||||
import com.ruoyi.iot.lock.domain.LockCommand;
|
||||
import com.ruoyi.iot.lock.domain.LockDevice;
|
||||
import com.ruoyi.iot.lock.domain.LockFace;
|
||||
@@ -59,6 +64,9 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
/** 整锁清空人脸时的人员编号占位:协议约定 00000000 表示删除该锁全部人脸 */
|
||||
private static final String CLEAR_ALL_PERSON_NO = "00000000";
|
||||
|
||||
/** 单次批量授权的门锁上限:同网关串行下发,数量过大时等待时间过长 */
|
||||
private static final int BATCH_MAX_DEVICES = 100;
|
||||
|
||||
@Autowired
|
||||
private LockAuthMapper authMapper;
|
||||
|
||||
@@ -95,12 +103,101 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
return authMapper.selectAuthList(auth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockAuthPerson> selectPersonAuthList(LockAuthPerson query)
|
||||
{
|
||||
return authMapper.selectPersonAuthList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockAuth selectAuthById(Long authId)
|
||||
{
|
||||
return authMapper.selectAuthById(authId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockAuth> selectExternalAuthList(LockAuth query)
|
||||
{
|
||||
return authMapper.selectExternalAuthList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockAuth> selectAllAuthList(LockAuth query)
|
||||
{
|
||||
return authMapper.selectAllAuthList(query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockAuth selectExternalAuthById(Long authId)
|
||||
{
|
||||
return authMapper.selectExternalAuthById(authId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockAuth selectExternalAuthForApp(Long authId, String appCode)
|
||||
{
|
||||
if (authId == null || StringUtils.isEmpty(appCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return authMapper.selectExternalAuthForApp(authId, appCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateExternalAuth(Long authId, LockAuth changes)
|
||||
{
|
||||
LockAuth auth = authMapper.selectAuthById(authId);
|
||||
if (auth == null || !LockConstants.AUTH_SOURCE_OPENAPI.equals(auth.getSource())
|
||||
|| "2".equals(auth.getDelFlag()))
|
||||
{
|
||||
throw new IllegalArgumentException("非外部授权,无法修改");
|
||||
}
|
||||
if (changes == null)
|
||||
{
|
||||
throw new IllegalArgumentException("修改内容不能为空");
|
||||
}
|
||||
if (changes.getBeginTime() != null && changes.getEndTime() != null
|
||||
&& changes.getBeginTime().after(changes.getEndTime()))
|
||||
{
|
||||
throw new IllegalArgumentException("生效时间不能晚于失效时间");
|
||||
}
|
||||
auth.setBeginTime(changes.getBeginTime());
|
||||
auth.setEndTime(changes.getEndTime());
|
||||
auth.setRemark(changes.getRemark());
|
||||
auth.setUpdateBy(changes.getUpdateBy());
|
||||
int rows = authMapper.updateExternalAuth(auth);
|
||||
// 已取消(4)没有设备侧权限,只保存;重新下发由页面单独触发
|
||||
if (!LockConstants.SYNC_CLEARED.equals(auth.getSyncStatus()))
|
||||
{
|
||||
pushInternal(auth, true);
|
||||
}
|
||||
log.info("外部授权修改有效期, authId={}, beginTime={}, endTime={}", authId, changes.getBeginTime(),
|
||||
changes.getEndTime());
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int deleteExternalAuthByIds(Long[] authIds)
|
||||
{
|
||||
if (authIds == null || authIds.length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// 整批校验:混入平台授权或已删除行时一条都不删
|
||||
for (Long authId : authIds)
|
||||
{
|
||||
LockAuth auth = authMapper.selectAuthById(authId);
|
||||
if (auth == null || !LockConstants.AUTH_SOURCE_OPENAPI.equals(auth.getSource())
|
||||
|| "2".equals(auth.getDelFlag()))
|
||||
{
|
||||
throw new IllegalArgumentException("所选授权中包含非外部授权,已取消删除,请刷新后重试");
|
||||
}
|
||||
}
|
||||
return deleteAuthByIds(authIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockAuth> selectAuthsByDeviceId(Long deviceId)
|
||||
{
|
||||
@@ -110,8 +207,29 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertAuth(LockAuth auth)
|
||||
{
|
||||
return doInsertAuth(auth, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertAuth(LockAuth auth, boolean adoptPlatformSource)
|
||||
{
|
||||
return doInsertAuth(auth, adoptPlatformSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单行新增/覆盖并下发。
|
||||
* <p>
|
||||
* 批量授权逐行直接调用本方法:不包整批事务,单锁失败不能回滚其它锁。
|
||||
* 插入时 sync_status 固定写 '0',即使下发前异常,补偿任务仍能捞回该行。
|
||||
*
|
||||
* @param adoptPlatformSource 覆盖时是否允许外部入住/换房接管平台行归属
|
||||
*/
|
||||
private int doInsertAuth(LockAuth auth, boolean adoptPlatformSource)
|
||||
{
|
||||
fillAuth(auth);
|
||||
applySourceDefaults(auth);
|
||||
if (StringUtils.isEmpty(auth.getStatus()))
|
||||
{
|
||||
// 前端表单没有状态项;insert 显式带列时 null 不会走列默认值,
|
||||
@@ -123,6 +241,7 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
LockAuth existing = findExistingAuth(auth);
|
||||
if (existing != null)
|
||||
{
|
||||
applyOverwriteSource(auth, existing, adoptPlatformSource);
|
||||
reviveAndPush(auth, existing);
|
||||
return 1;
|
||||
}
|
||||
@@ -140,11 +259,205 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
applyOverwriteSource(auth, conflict, adoptPlatformSource);
|
||||
reviveAndPush(auth, conflict);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 来源默认值:未指定即平台;非开放接口来源不保留应用与业务号 */
|
||||
private void applySourceDefaults(LockAuth auth)
|
||||
{
|
||||
if (StringUtils.isEmpty(auth.getSource()))
|
||||
{
|
||||
auth.setSource(LockConstants.AUTH_SOURCE_PLATFORM);
|
||||
}
|
||||
if (!LockConstants.AUTH_SOURCE_OPENAPI.equals(auth.getSource()))
|
||||
{
|
||||
auth.setSourceAppCode(null);
|
||||
auth.setSourceBizNo(null);
|
||||
}
|
||||
else if (StringUtils.isEmpty(auth.getSourceBizNo()))
|
||||
{
|
||||
// 业务号为空时统一落 NULL,避免与未传值产生两种“空”
|
||||
auth.setSourceBizNo(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 覆盖已有授权行时的归属判定。
|
||||
* <p>
|
||||
* 平台覆盖不夺取外部归属;外部来源只允许同应用覆盖,跨应用拒绝;
|
||||
* 外部入住/换房(adoptPlatformSource=true)可以接管平台行,直接授权接口不可以。
|
||||
*/
|
||||
private void applyOverwriteSource(LockAuth auth, LockAuth existing, boolean adoptPlatformSource)
|
||||
{
|
||||
if (!LockConstants.AUTH_SOURCE_OPENAPI.equals(auth.getSource()))
|
||||
{
|
||||
// 平台覆盖:reviveAuth 不改 source 列,外部归属自然保留
|
||||
return;
|
||||
}
|
||||
String appCode = StringUtils.trim(auth.getSourceAppCode());
|
||||
String existingSource = StringUtils.isEmpty(existing.getSource())
|
||||
? LockConstants.AUTH_SOURCE_PLATFORM : existing.getSource();
|
||||
String existingApp = StringUtils.trim(existing.getSourceAppCode());
|
||||
if (LockConstants.AUTH_SOURCE_OPENAPI.equals(existingSource))
|
||||
{
|
||||
if (StringUtils.isNotEmpty(existingApp) && !existingApp.equals(appCode))
|
||||
{
|
||||
throw new IllegalArgumentException("该人员在此门锁上已有其他应用的授权");
|
||||
}
|
||||
}
|
||||
else if (!adoptPlatformSource)
|
||||
{
|
||||
throw new IllegalArgumentException("该人员在此门锁上已有平台授权,请先在授权管理处理");
|
||||
}
|
||||
// 同应用外部行:刷新业务号;平台行被接管:写入来源应用
|
||||
LockAuth sourceUpdate = new LockAuth();
|
||||
sourceUpdate.setAuthId(existing.getAuthId());
|
||||
sourceUpdate.setSource(LockConstants.AUTH_SOURCE_OPENAPI);
|
||||
sourceUpdate.setSourceAppCode(appCode);
|
||||
sourceUpdate.setSourceBizNo(auth.getSourceBizNo());
|
||||
sourceUpdate.setUpdateBy(auth.getUpdateBy());
|
||||
authMapper.updateAuthSource(sourceUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockAuthBatchResult batchInsertAuth(LockAuthBatchRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new IllegalArgumentException("批量授权参数不能为空");
|
||||
}
|
||||
String personNo = StringUtils.trim(request.getPersonNo());
|
||||
if (StringUtils.isEmpty(personNo))
|
||||
{
|
||||
throw new IllegalArgumentException("人员编号不能为空");
|
||||
}
|
||||
// 人员不存在时整批拒绝:没有姓名快照,生成 N 行也下发不了
|
||||
if (personMapper.selectPersonByNo(personNo) == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人员不存在,请先建立人员档案,人员编号=" + personNo);
|
||||
}
|
||||
String authType = StringUtils.trim(request.getAuthType());
|
||||
if (StringUtils.isEmpty(authType))
|
||||
{
|
||||
throw new IllegalArgumentException("授权类型不能为空");
|
||||
}
|
||||
if (!LockConstants.AUTH_FACE.equals(authType) && !LockConstants.AUTH_PASSWORD.equals(authType)
|
||||
&& !LockConstants.AUTH_FINGERPRINT.equals(authType) && !LockConstants.AUTH_IC_CARD.equals(authType))
|
||||
{
|
||||
throw new IllegalArgumentException("不支持的授权类型:" + authType);
|
||||
}
|
||||
if (request.getBeginTime() != null && request.getEndTime() != null
|
||||
&& request.getBeginTime().after(request.getEndTime()))
|
||||
{
|
||||
throw new IllegalArgumentException("生效时间不能晚于失效时间");
|
||||
}
|
||||
LinkedHashSet<Long> deviceIds = new LinkedHashSet<Long>();
|
||||
if (request.getDeviceIds() != null)
|
||||
{
|
||||
for (Long deviceId : request.getDeviceIds())
|
||||
{
|
||||
if (deviceId != null)
|
||||
{
|
||||
deviceIds.add(deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deviceIds.isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException("请选择授权门锁");
|
||||
}
|
||||
if (deviceIds.size() > BATCH_MAX_DEVICES)
|
||||
{
|
||||
throw new IllegalArgumentException("单次最多授权 " + BATCH_MAX_DEVICES + " 把门锁");
|
||||
}
|
||||
boolean face = LockConstants.AUTH_FACE.equals(authType);
|
||||
if (!face && StringUtils.isEmpty(request.getAuthValue()))
|
||||
{
|
||||
throw new IllegalArgumentException("授权内容不能为空");
|
||||
}
|
||||
|
||||
LockAuthBatchResult result = new LockAuthBatchResult();
|
||||
result.setTotal(deviceIds.size());
|
||||
List<LockAuthBatchResult.Item> items = new ArrayList<LockAuthBatchResult.Item>();
|
||||
int created = 0;
|
||||
int overwritten = 0;
|
||||
int failed = 0;
|
||||
for (Long deviceId : deviceIds)
|
||||
{
|
||||
LockAuthBatchResult.Item item = new LockAuthBatchResult.Item();
|
||||
item.setDeviceId(deviceId);
|
||||
LockDevice device = deviceMapper.selectDeviceById(deviceId);
|
||||
if (device == null)
|
||||
{
|
||||
item.setAction(LockAuthBatchResult.ACTION_FAILED);
|
||||
item.setSyncMsg("门锁不存在,无法授权");
|
||||
failed++;
|
||||
items.add(item);
|
||||
continue;
|
||||
}
|
||||
item.setDeviceSn(device.getDeviceSn());
|
||||
item.setDeviceName(device.getDeviceName());
|
||||
LockAuth auth = null;
|
||||
try
|
||||
{
|
||||
auth = new LockAuth();
|
||||
auth.setPersonNo(personNo);
|
||||
auth.setAuthType(authType);
|
||||
// 人脸特征由下发时按各锁算法版本取,模板值一律不落库,避免跨版本错配
|
||||
auth.setAuthValue(face ? null : request.getAuthValue());
|
||||
auth.setBeginTime(request.getBeginTime());
|
||||
auth.setEndTime(request.getEndTime());
|
||||
auth.setRemark(request.getRemark());
|
||||
auth.setDeviceId(deviceId);
|
||||
boolean existed = findExistingAuth(auth) != null;
|
||||
doInsertAuth(auth, false);
|
||||
item.setAuthId(auth.getAuthId());
|
||||
item.setAction(existed ? LockAuthBatchResult.ACTION_OVERWRITTEN
|
||||
: LockAuthBatchResult.ACTION_CREATED);
|
||||
LockAuth saved = authMapper.selectAuthById(auth.getAuthId());
|
||||
if (saved != null)
|
||||
{
|
||||
item.setSyncStatus(saved.getSyncStatus());
|
||||
item.setSyncMsg(saved.getSyncMsg());
|
||||
}
|
||||
if (existed)
|
||||
{
|
||||
overwritten++;
|
||||
}
|
||||
else
|
||||
{
|
||||
created++;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
String message = StringUtils.substring(e.getMessage(), 0, 500);
|
||||
if (auth != null && auth.getAuthId() != null)
|
||||
{
|
||||
// 行已落库但下发抛异常:置失败交给补偿重发,避免停在「已下发」不再重试
|
||||
authMapper.updateSyncStatus(auth.getAuthId(), LockConstants.SYNC_FAIL, message);
|
||||
item.setAuthId(auth.getAuthId());
|
||||
}
|
||||
item.setAction(LockAuthBatchResult.ACTION_FAILED);
|
||||
item.setSyncMsg(message);
|
||||
failed++;
|
||||
log.warn("批量授权单锁处理失败, personNo={}, deviceId={}, msg={}", personNo, deviceId,
|
||||
e.getMessage());
|
||||
}
|
||||
items.add(item);
|
||||
}
|
||||
result.setItems(items);
|
||||
result.setCreated(created);
|
||||
result.setOverwritten(overwritten);
|
||||
result.setFailed(failed);
|
||||
log.info("多锁批量授权完成, personNo={}, authType={}, total={}, created={}, overwritten={}, failed={}",
|
||||
personNo, authType, result.getTotal(), created, overwritten, failed);
|
||||
return result;
|
||||
}
|
||||
|
||||
private LockAuth findExistingAuth(LockAuth auth)
|
||||
{
|
||||
return authMapper.selectAuthByUniqueKey(auth.getPersonNo(), auth.getDeviceId(), auth.getAuthType());
|
||||
@@ -206,6 +519,83 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
return authMapper.deleteAuthByIds(authIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int deleteAuthRecordsByIds(Long[] authIds)
|
||||
{
|
||||
if (authIds == null || authIds.length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
for (Long authId : authIds)
|
||||
{
|
||||
LockAuth auth = authMapper.selectAuthById(authId);
|
||||
if (auth == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!LockConstants.SYNC_CLEARED.equals(auth.getSyncStatus()))
|
||||
{
|
||||
// 设备侧还有权限时删记录,会在门锁上留下无法追溯的开门凭据
|
||||
throw new IllegalArgumentException("授权[" + auth.getAuthNo() + "]设备侧未清除,请使用删除授权");
|
||||
}
|
||||
}
|
||||
return authMapper.deleteAuthByIds(authIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int deleteAuthsByPersonNos(String[] personNos)
|
||||
{
|
||||
String[] normalized = normalizePersonNos(personNos);
|
||||
if (normalized.length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
List<Long> authIds = authMapper.selectAuthIdsByPersonNos(normalized);
|
||||
if (authIds.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
// 复用逐条删除:先向门锁下发清除,再逻辑删除,保证设备侧权限一并回收
|
||||
return deleteAuthByIds(authIds.toArray(new Long[authIds.size()]));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchPushByPersonNos(String[] personNos)
|
||||
{
|
||||
String[] normalized = normalizePersonNos(personNos);
|
||||
if (normalized.length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<Long> authIds = authMapper.selectPushableAuthIdsByPersonNos(normalized);
|
||||
if (authIds.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
batchPush(authIds.toArray(new Long[authIds.size()]));
|
||||
}
|
||||
|
||||
/** 去掉空白人员编号并去重,避免拼出空的 IN 条件 */
|
||||
private String[] normalizePersonNos(String[] personNos)
|
||||
{
|
||||
if (personNos == null || personNos.length == 0)
|
||||
{
|
||||
return new String[0];
|
||||
}
|
||||
LinkedHashSet<String> unique = new LinkedHashSet<String>();
|
||||
for (String personNo : personNos)
|
||||
{
|
||||
String trimmed = StringUtils.trim(personNo);
|
||||
if (StringUtils.isNotEmpty(trimmed))
|
||||
{
|
||||
unique.add(trimmed);
|
||||
}
|
||||
}
|
||||
return unique.toArray(new String[unique.size()]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void push(Long authId)
|
||||
{
|
||||
@@ -380,9 +770,9 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
}
|
||||
if (!success && LockCommandPolicy.isBusy(message))
|
||||
{
|
||||
// 设备忙是瞬时状态,由命令层 3 秒后重发;这里若把授权置失败,
|
||||
// 授权补偿会再建一条命令,同一张脸被重复下发
|
||||
return;
|
||||
// 网关忙在命令层已排队重发,不会走到这里;能走到的是重排次数用尽的终态,
|
||||
// 此时必须把授权置失败,交给补偿重新建档,否则授权会停在「已下发」无人跟进
|
||||
log.warn("授权命令因网关忙终结, authId={}, msg={}", auth.getAuthId(), message);
|
||||
}
|
||||
String syncStatus;
|
||||
if (LockConstants.CMD_AUTH_CLEAR.equals(command.getCommandType()))
|
||||
@@ -557,6 +947,12 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
return;
|
||||
}
|
||||
Long groupId = target.getDeviceRef().getGroupId();
|
||||
if (commandMapper.countInflightAuthByBizId(auth.getAuthId()) > 0)
|
||||
{
|
||||
// 同一授权已有命令在途(含等待门锁终态、网关忙排队),补偿不能另建一条
|
||||
log.info("授权已有在途命令,本轮跳过, authId={}, groupId={}", auth.getAuthId(), groupId);
|
||||
return;
|
||||
}
|
||||
if (commandMapper.countInflightAuthCommands(groupId) > 0)
|
||||
{
|
||||
// 网关同一时刻只处理一条 data/batch,并发下发会直接回 gw busy。
|
||||
@@ -702,7 +1098,7 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
* </pre>
|
||||
*
|
||||
* @param add true 下发授权(add 动作),false 清除授权(delete 动作)
|
||||
* @param authValue 实际下发的授权内容(人脸已按门锁算法版本选值),删除帧不使用
|
||||
* @param authValue 实际下发的授权内容(人脸已按门锁算法版本选值);密码/IC卡删除时用它定位凭据
|
||||
*/
|
||||
private YdlinkFrameDTO buildAuthFrame(LockAuth auth, LockDevice device, RouteTarget target, boolean add,
|
||||
String authValue)
|
||||
@@ -720,6 +1116,9 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
String value = StringUtils.defaultString(authValue);
|
||||
// 人脸帧比密码等帧多一个「授权锁标识」位(与人员编号同值),长度不同不可混用
|
||||
boolean face = YdlinkOp.FACE_FEATURE.equals(op);
|
||||
// 只有人脸/指纹按人员编号建索引;密码与 IC 卡以凭据本身标识,
|
||||
// 帧里不能塞人员编号,否则设备侧按人员找不到对应凭据
|
||||
boolean personIdAuth = face || YdlinkOp.FINGERPRINT.equals(op);
|
||||
if (add)
|
||||
{
|
||||
if (face)
|
||||
@@ -728,18 +1127,32 @@ public class LockAuthServiceImpl implements ILockAuthService
|
||||
frame.setFlatParams(Arrays.asList("add", personNo, value, personNo,
|
||||
format(auth.getBeginTime()), format(auth.getEndTime()), deviceSn, devicePsw));
|
||||
}
|
||||
else if (personIdAuth)
|
||||
{
|
||||
// 指纹:[动作, 人员编号, 特征值, 开始时间, 结束时间, 门锁SN, 门锁密码]
|
||||
frame.setFlatParams(Arrays.asList("add", personNo, value,
|
||||
format(auth.getBeginTime()), format(auth.getEndTime()), deviceSn, devicePsw));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 密码/指纹/IC卡:[动作, 人员编号, 授权内容, 开始时间, 结束时间, 门锁SN, 门锁密码]
|
||||
frame.setFlatParams(Arrays.asList("add", personNo, value,
|
||||
// 密码/IC卡:[动作, 凭据, 凭据, 开始时间, 结束时间, 门锁SN, 门锁密码]
|
||||
frame.setFlatParams(Arrays.asList("add", value, value,
|
||||
format(auth.getBeginTime()), format(auth.getEndTime()), deviceSn, devicePsw));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 删除帧只需定位「删除谁 + 哪把锁」:["delete", 人员编号, 门锁SN, 门锁密码]
|
||||
// 实测人脸删除报文即此 4 项,不携带特征值与时间
|
||||
frame.setFlatParams(Arrays.asList("delete", personNo, deviceSn, devicePsw));
|
||||
if (personIdAuth)
|
||||
{
|
||||
// 删除帧只需定位「删除谁 + 哪把锁」:["delete", 人员编号, 门锁SN, 门锁密码]
|
||||
// 实测人脸删除报文即此 4 项,不携带特征值与时间
|
||||
frame.setFlatParams(Arrays.asList("delete", personNo, deviceSn, devicePsw));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 密码/IC卡删除比人脸多一个凭据位(协议 5 项),用凭据定位要删的条目
|
||||
frame.setFlatParams(Arrays.asList("delete", value, value, deviceSn, devicePsw));
|
||||
}
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
+173
-37
@@ -2,7 +2,9 @@ package com.ruoyi.iot.lock.service.impl;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -174,6 +176,52 @@ public class LockCommandServiceImpl implements ILockCommandService
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public String handleGatewayBusy(String topic, String seq, String payload, String message)
|
||||
{
|
||||
if (StringUtils.isEmpty(seq))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
LockCommand command = commandMapper.selectCommandByNo(seq);
|
||||
if (command == null)
|
||||
{
|
||||
log.warn("未匹配到命令, seq={}, topic={}", seq, topic);
|
||||
return null;
|
||||
}
|
||||
if (isFinished(command))
|
||||
{
|
||||
// 已有终态(例如 0 先到),迟到的网关忙不能改写结果
|
||||
return null;
|
||||
}
|
||||
if (LockConstants.ACK_YES.equals(command.getGatewayAck()))
|
||||
{
|
||||
// 已收到 10 说明报文已转发,此后再来的忙响应不能推翻转发事实,继续等门锁终态
|
||||
log.warn("已收到网关回执后又收到忙响应,保持等待门锁终态, seq={}", seq);
|
||||
return null;
|
||||
}
|
||||
command.setRespTopic(topic);
|
||||
command.setRespPayload(YdlinkPayloadMasker.maskPayload(payload));
|
||||
command.setRespTime(new Date());
|
||||
Integer retryCount = command.getRetryCount() == null ? 0 : command.getRetryCount();
|
||||
String reason = StringUtils.isEmpty(message) ? "网关忙" : message;
|
||||
if (LockCommandPolicy.isRetryDisabled(command.getCommandType())
|
||||
|| retryCount >= LockCommandPolicy.maxBusyRetry(command.getCommandType()))
|
||||
{
|
||||
return finish(command, false, reason + ",重排次数已用尽");
|
||||
}
|
||||
// 11 表示报文未转发到门锁:回到待发送并排队,但仍算在途,同网关后续命令不得插队
|
||||
command.setCommandStatus(LockConstants.STATUS_PENDING);
|
||||
command.setRetryCount(retryCount + 1);
|
||||
command.setNextRetryTime(LockCommandPolicy.busyRetryTime());
|
||||
command.setErrorMsg(StringUtils.substring(reason, 0, 500));
|
||||
commandMapper.updateCommand(command);
|
||||
log.info("网关忙,命令排队重发, seq={}, commandType={}, retryCount={}", seq, command.getCommandType(),
|
||||
retryCount + 1);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令是否已有终态结果
|
||||
*/
|
||||
@@ -225,16 +273,6 @@ public class LockCommandServiceImpl implements ILockCommandService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权类命令是否发到分组主题:同一网关内只能串行,并发会被回 gw busy
|
||||
*/
|
||||
private boolean isGroupAuthCommand(LockCommand command)
|
||||
{
|
||||
return LockConstants.TARGET_GROUP.equals(command.getTargetType())
|
||||
&& (LockConstants.CMD_AUTH_PUSH.equals(command.getCommandType())
|
||||
|| LockConstants.CMD_AUTH_CLEAR.equals(command.getCommandType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权命令终结为超时时回写授权同步状态
|
||||
* <p>
|
||||
@@ -287,8 +325,24 @@ public class LockCommandServiceImpl implements ILockCommandService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
long timeoutMillis = LockCommandPolicy.timeoutSeconds(command.getCommandType()) * 1000L;
|
||||
if (command.getReqTime().getTime() + timeoutMillis > now)
|
||||
// 网关已回 10 表示报文已转发,此后只等门锁终态:从网关回执起算独立等待时长,
|
||||
// 期间既不重发也不判失败,否则会把正在执行/已成功的授权重复下发或提前放行下一条
|
||||
boolean waitingDevice = LockConstants.ACK_YES.equals(command.getGatewayAck())
|
||||
&& !LockConstants.ACK_YES.equals(command.getDeviceAck());
|
||||
long baseTime;
|
||||
long timeoutMillis;
|
||||
if (waitingDevice)
|
||||
{
|
||||
baseTime = command.getRespTime() == null ? command.getReqTime().getTime()
|
||||
: command.getRespTime().getTime();
|
||||
timeoutMillis = LockCommandPolicy.deviceWaitSeconds(command.getCommandType()) * 1000L;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseTime = command.getReqTime().getTime();
|
||||
timeoutMillis = LockCommandPolicy.timeoutSeconds(command.getCommandType()) * 1000L;
|
||||
}
|
||||
if (baseTime + timeoutMillis > now)
|
||||
{
|
||||
// 尚未到期,下一轮再判
|
||||
continue;
|
||||
@@ -297,7 +351,13 @@ public class LockCommandServiceImpl implements ILockCommandService
|
||||
Integer retryCount = command.getRetryCount() == null ? 0 : command.getRetryCount();
|
||||
LockCommand update = new LockCommand();
|
||||
update.setCommandId(command.getCommandId());
|
||||
if (LockCommandPolicy.isRetryDisabled(command.getCommandType()))
|
||||
if (waitingDevice)
|
||||
{
|
||||
// 已转发到门锁:不再重发,等不到终态即判超时,由授权补偿决定是否重新建档下发
|
||||
update.setCommandStatus(LockConstants.STATUS_TIMEOUT);
|
||||
update.setErrorMsg("网关已接收,等待门锁回执超时");
|
||||
}
|
||||
else if (LockCommandPolicy.isRetryDisabled(command.getCommandType()))
|
||||
{
|
||||
// 该类命令不自动重发,超时即终态
|
||||
update.setCommandStatus(LockConstants.STATUS_TIMEOUT);
|
||||
@@ -328,25 +388,87 @@ public class LockCommandServiceImpl implements ILockCommandService
|
||||
@Override
|
||||
public int retryPending()
|
||||
{
|
||||
LockCommand query = new LockCommand();
|
||||
query.setCommandStatus(LockConstants.STATUS_FAIL);
|
||||
List<LockCommand> commands = commandMapper.selectCommandList(query);
|
||||
int count = 0;
|
||||
Date now = new Date();
|
||||
for (LockCommand command : commands)
|
||||
// 1) 网关忙排队的命令:每轮每个主题(同网关)只放行一条,且网关无已发送命令时才重发,
|
||||
// 避免多条排队命令互相等待或同时挤进网关
|
||||
Set<String> republishedTopics = new HashSet<String>();
|
||||
for (LockCommand command : commandMapper.selectDeferredCommands(now))
|
||||
{
|
||||
String topic = command.getTopic();
|
||||
if (topic == null || republishedTopics.contains(topic))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (LockCommandPolicy.isRetryDisabled(command.getCommandType()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Integer busyRetry = command.getRetryCount() == null ? 0 : command.getRetryCount();
|
||||
if (busyRetry >= LockCommandPolicy.maxBusyRetry(command.getCommandType()))
|
||||
{
|
||||
LockCommand expired = new LockCommand();
|
||||
expired.setCommandId(command.getCommandId());
|
||||
expired.setCommandStatus(LockConstants.STATUS_TIMEOUT);
|
||||
expired.setErrorMsg("网关持续忙,超过最大重排次数");
|
||||
commandMapper.updateCommand(expired);
|
||||
markAuthTerminal(command, expired.getErrorMsg());
|
||||
continue;
|
||||
}
|
||||
if (commandMapper.countOtherSentByTopic(topic, command.getCommandId()) > 0)
|
||||
{
|
||||
// 同网关已有命令在发送/等待门锁终态,等它终结再重发
|
||||
continue;
|
||||
}
|
||||
if (republish(command, busyRetry))
|
||||
{
|
||||
republishedTopics.add(topic);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
// 2) 发送失败或等待超时的命令按退避重发
|
||||
LockCommand query = new LockCommand();
|
||||
query.setCommandStatus(LockConstants.STATUS_FAIL);
|
||||
for (LockCommand command : commandMapper.selectCommandList(query))
|
||||
{
|
||||
if (command.getNextRetryTime() == null || command.getNextRetryTime().after(now))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
boolean waitingDevice = LockConstants.ACK_YES.equals(command.getGatewayAck())
|
||||
&& !LockConstants.ACK_YES.equals(command.getDeviceAck());
|
||||
if (waitingDevice)
|
||||
{
|
||||
// 已收到网关 10:历史数据被旧逻辑判成失败后不能重发,回到等待态由超时判定收口
|
||||
long base = command.getRespTime() != null ? command.getRespTime().getTime()
|
||||
: (command.getReqTime() == null ? now.getTime() : command.getReqTime().getTime());
|
||||
long waitMillis = LockCommandPolicy.deviceWaitSeconds(command.getCommandType()) * 1000L;
|
||||
if (base + waitMillis <= now.getTime())
|
||||
{
|
||||
LockCommand expired = new LockCommand();
|
||||
expired.setCommandId(command.getCommandId());
|
||||
expired.setCommandStatus(LockConstants.STATUS_TIMEOUT);
|
||||
expired.setErrorMsg("网关已接收,等待门锁回执超时");
|
||||
commandMapper.updateCommand(expired);
|
||||
markAuthTerminal(command, expired.getErrorMsg());
|
||||
}
|
||||
else
|
||||
{
|
||||
LockCommand waiting = new LockCommand();
|
||||
waiting.setCommandId(command.getCommandId());
|
||||
waiting.setCommandStatus(LockConstants.STATUS_SENT);
|
||||
commandMapper.updateCommand(waiting);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (LockCommandPolicy.isRetryDisabled(command.getCommandType()))
|
||||
{
|
||||
// 时间同步、远程开关锁失败后不自动重发
|
||||
continue;
|
||||
}
|
||||
if (isGroupAuthCommand(command) && commandMapper.countInflightAuthCommands(command.getTargetId()) > 0)
|
||||
if (commandMapper.countOtherInflightByTopic(command.getTopic(), command.getCommandId()) > 0)
|
||||
{
|
||||
// 同网关仍有在途授权命令,本轮跳过,避免再次撞上 gw busy
|
||||
// 同网关仍有在途命令,本轮跳过,避免再次撞上 gw busy
|
||||
continue;
|
||||
}
|
||||
Integer maxRetry = command.getMaxRetry() == null ? DEFAULT_MAX_RETRY : command.getMaxRetry();
|
||||
@@ -362,31 +484,45 @@ public class LockCommandServiceImpl implements ILockCommandService
|
||||
markAuthTerminal(command, expired.getErrorMsg());
|
||||
continue;
|
||||
}
|
||||
LockCommand update = new LockCommand();
|
||||
update.setCommandId(command.getCommandId());
|
||||
update.setRetryCount(retryCount + 1);
|
||||
update.setNextRetryTime(LockCommandPolicy.nextRetryTime(retryCount + 1));
|
||||
// 先置已发送再发布,避免应答先到被覆盖
|
||||
update.setCommandStatus(LockConstants.STATUS_SENT);
|
||||
update.setReqTime(now);
|
||||
commandMapper.updateCommand(update);
|
||||
try
|
||||
if (republish(command, retryCount + 1))
|
||||
{
|
||||
mqttService.publish(command.getTopic(), command.getPayload(), 1, false);
|
||||
count++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LockCommand failed = new LockCommand();
|
||||
failed.setCommandId(command.getCommandId());
|
||||
failed.setCommandStatus(LockConstants.STATUS_FAIL);
|
||||
failed.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 500));
|
||||
commandMapper.updateCommand(failed);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重发已落库的原始报文(seq/payload 不变),先把状态置为已发送再发布,避免应答先到被覆盖
|
||||
*/
|
||||
private boolean republish(LockCommand command, int retryCount)
|
||||
{
|
||||
LockCommand update = new LockCommand();
|
||||
update.setCommandId(command.getCommandId());
|
||||
update.setRetryCount(retryCount);
|
||||
update.setNextRetryTime(LockCommandPolicy.nextRetryTime(retryCount));
|
||||
update.setCommandStatus(LockConstants.STATUS_SENT);
|
||||
update.setReqTime(new Date());
|
||||
// 清掉上一次尝试的回执标记:否则旧的 deviceAck/deviceResult 会被当成这次的结果
|
||||
update.setGatewayAck(LockConstants.ACK_NO);
|
||||
update.setDeviceAck(LockConstants.ACK_NO);
|
||||
commandMapper.updateCommand(update);
|
||||
try
|
||||
{
|
||||
mqttService.publish(command.getTopic(), command.getPayload(), 1, false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LockCommand failed = new LockCommand();
|
||||
failed.setCommandId(command.getCommandId());
|
||||
failed.setCommandStatus(LockConstants.STATUS_FAIL);
|
||||
failed.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 500));
|
||||
commandMapper.updateCommand(failed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void retry(Long commandId)
|
||||
|
||||
+11
-1
@@ -354,15 +354,25 @@ public class LockGatewayServiceImpl implements ILockGatewayService
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (LockConstants.OFFLINE.equals(onlineStatus))
|
||||
{
|
||||
// 离线通知不动 last_online_time,保留最后一次真正在线的时刻
|
||||
gatewayMapper.updateOnlineStatusOnly(gateway.getGatewayId(), onlineStatus);
|
||||
return;
|
||||
}
|
||||
gatewayMapper.updateOnlineStatus(gateway.getGatewayId(), onlineStatus, new Date());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int markOffline(int offlineMinutes)
|
||||
{
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.MINUTE, -Math.max(offlineMinutes, 1));
|
||||
int count = gatewayMapper.markOfflineBefore(calendar.getTime(), new Date());
|
||||
Date offlineTime = calendar.getTime();
|
||||
// 先按归属行的最近上报刷新在线:网关转发门锁数据即证明在线,不能只依赖 onoff_line 连接事件
|
||||
gatewayMapper.refreshOnlineByReport(offlineTime);
|
||||
int count = gatewayMapper.markOfflineBefore(offlineTime, new Date());
|
||||
// 网关离线后,其视角下的门锁归属状态同步置离线;其它网关的归属不受影响
|
||||
relationMapper.markOfflineByOfflineGateways();
|
||||
return count;
|
||||
|
||||
+21
-6
@@ -83,6 +83,9 @@ public class LockStayServiceImpl implements ILockStayService
|
||||
/** 单据来源:平台办理 */
|
||||
private static final String STAY_SOURCE_PLATFORM = "1";
|
||||
|
||||
/** 单据来源:开放接口办理 */
|
||||
private static final String STAY_SOURCE_API = "2";
|
||||
|
||||
/** 删除标志:已删除 */
|
||||
private static final String DEL_FLAG_DELETED = "2";
|
||||
|
||||
@@ -363,6 +366,8 @@ public class LockStayServiceImpl implements ILockStayService
|
||||
}
|
||||
Date now = new Date();
|
||||
String stayNo = nextStayNo();
|
||||
String staySource = StringUtils.isEmpty(oldStay.getSource()) ? STAY_SOURCE_PLATFORM : oldStay.getSource();
|
||||
boolean external = STAY_SOURCE_API.equals(staySource);
|
||||
LockAuth auth = new LockAuth();
|
||||
auth.setPersonNo(person.getPersonNo());
|
||||
auth.setPersonName(person.getPersonName());
|
||||
@@ -371,7 +376,11 @@ public class LockStayServiceImpl implements ILockStayService
|
||||
auth.setBeginTime(now);
|
||||
auth.setEndTime(newEnd);
|
||||
auth.setRemark("换房 " + stayNo);
|
||||
authService.insertAuth(auth);
|
||||
// 换房新授权继承原单来源应用;biz_no 不继承((biz_system,biz_no) 唯一),业务号留空
|
||||
auth.setSource(staySource);
|
||||
auth.setSourceAppCode(external ? oldStay.getBizSystem() : null);
|
||||
auth.setSourceBizNo(null);
|
||||
authService.insertAuth(auth, external);
|
||||
|
||||
LockStay newStay = new LockStay();
|
||||
newStay.setStayNo(stayNo);
|
||||
@@ -704,6 +713,11 @@ public class LockStayServiceImpl implements ILockStayService
|
||||
}
|
||||
|
||||
String stayNo = nextStayNo();
|
||||
String bizNo = StringUtils.trim(body.getBizNo());
|
||||
String bizSystem = StringUtils.trim(body.getBizSystem());
|
||||
String source = StringUtils.isEmpty(StringUtils.trim(body.getSource())) ? STAY_SOURCE_PLATFORM
|
||||
: StringUtils.trim(body.getSource());
|
||||
boolean external = STAY_SOURCE_API.equals(source);
|
||||
LockAuth auth = new LockAuth();
|
||||
auth.setPersonNo(person.getPersonNo());
|
||||
auth.setPersonName(person.getPersonName());
|
||||
@@ -714,7 +728,11 @@ public class LockStayServiceImpl implements ILockStayService
|
||||
auth.setBeginTime(begin);
|
||||
auth.setEndTime(body.getEndTime());
|
||||
auth.setRemark("入住 " + stayNo);
|
||||
authService.insertAuth(auth);
|
||||
// 授权归属随入住单来源;外部入住写入应用与业务号,便于页面按应用追溯
|
||||
auth.setSource(source);
|
||||
auth.setSourceAppCode(external && StringUtils.isNotEmpty(bizSystem) ? bizSystem : null);
|
||||
auth.setSourceBizNo(external && StringUtils.isNotEmpty(bizNo) ? bizNo : null);
|
||||
authService.insertAuth(auth, external);
|
||||
|
||||
LockStay stay = new LockStay();
|
||||
stay.setStayNo(stayNo);
|
||||
@@ -733,15 +751,12 @@ public class LockStayServiceImpl implements ILockStayService
|
||||
stay.setStayStatus(STAY_STATUS_ACTIVE);
|
||||
stay.setAuthStatus(LockConstants.SYNC_PENDING);
|
||||
stay.setRemark(body.getRemark());
|
||||
String bizNo = StringUtils.trim(body.getBizNo());
|
||||
String bizSystem = StringUtils.trim(body.getBizSystem());
|
||||
// uk_lock_stay_biz(biz_system, biz_no) 只允许多行 NULL,空串会互相撞键,
|
||||
// 平台手工单必须落 NULL,否则第二次办理入住即 Duplicate entry ''
|
||||
stay.setBizNo(StringUtils.isEmpty(bizNo) ? null : bizNo);
|
||||
stay.setBizSystem(StringUtils.isEmpty(bizSystem) ? null : bizSystem);
|
||||
// insertStay 显式列出 source 列,为空会写入 NULL 而不是列默认值 '1',这里补默认
|
||||
stay.setSource(StringUtils.isEmpty(StringUtils.trim(body.getSource())) ? STAY_SOURCE_PLATFORM
|
||||
: StringUtils.trim(body.getSource()));
|
||||
stay.setSource(source);
|
||||
stayMapper.insertStay(stay);
|
||||
|
||||
// 授权可能因门锁无路由/缺特征在创建时即失败,回读真实状态写入入住单
|
||||
|
||||
+4
-1
@@ -9,6 +9,9 @@ import com.ruoyi.iot.lock.service.ILockGatewayService;
|
||||
|
||||
/**
|
||||
* 网关离线检测:超过指定时间未上报即置为离线
|
||||
* <p>
|
||||
* 在线来源有两个:网关级 onoff_line 连接通知,以及该网关归属门锁的周期上报(lockrssi 每分钟一次)。
|
||||
* 二者任一在窗口内出现即视为在线,因此检测周期取 1 分钟,与门锁上报节奏对齐。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@@ -31,7 +34,7 @@ public class GatewayOfflineCheckTask
|
||||
@Autowired
|
||||
private DistributedTaskLock taskLock;
|
||||
|
||||
@Scheduled(initialDelay = 60000L, fixedDelay = 300000L)
|
||||
@Scheduled(initialDelay = 30000L, fixedDelay = 60000L)
|
||||
public void execute()
|
||||
{
|
||||
DistributedTaskLock.LockHandle lock = taskLock.tryLock(LOCK_NAME, LOCK_TTL_SECONDS);
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ public class LockCommandRetryTask
|
||||
private DistributedTaskLock taskLock;
|
||||
|
||||
/**
|
||||
* 5 秒一轮:gw busy 重排 3 秒、退避首档 5 秒,排队的授权命令也靠这一轮接着下发,
|
||||
* 周期过长会让并发下发的第二张脸实际延迟到十几秒以上
|
||||
* 5 秒一轮:gw busy 重排 8 秒,排队的授权命令也靠这一轮接着下发;
|
||||
* 同一网关严格串行,宁可慢一点,也要等上一条拿到门锁终态再发下一条
|
||||
*/
|
||||
@Scheduled(initialDelay = 5000L, fixedDelay = 5000L)
|
||||
public void execute()
|
||||
|
||||
+42
-10
@@ -3,6 +3,7 @@ package com.ruoyi.iot.openapi.controller;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -23,6 +24,7 @@ import com.ruoyi.iot.lock.service.ILockAuthService;
|
||||
import com.ruoyi.iot.lock.service.ILockDeviceService;
|
||||
import com.ruoyi.iot.lock.service.ILockFaceService;
|
||||
import com.ruoyi.iot.openapi.domain.OpenAuthRequest;
|
||||
import com.ruoyi.iot.openapi.filter.OpenApiAuthFilter;
|
||||
|
||||
/**
|
||||
* 开放接口-门禁授权
|
||||
@@ -51,8 +53,13 @@ public class OpenAuthApiController
|
||||
*/
|
||||
@Anonymous
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody OpenAuthRequest request)
|
||||
public AjaxResult add(@RequestBody OpenAuthRequest request, HttpServletRequest httpRequest)
|
||||
{
|
||||
String appCode = resolveAppCode(httpRequest);
|
||||
if (StringUtils.isEmpty(appCode))
|
||||
{
|
||||
return AjaxResult.error("应用身份缺失");
|
||||
}
|
||||
if (request == null || StringUtils.isEmpty(request.getPersonNo()))
|
||||
{
|
||||
return AjaxResult.error("人员编号不能为空");
|
||||
@@ -81,6 +88,10 @@ public class OpenAuthApiController
|
||||
auth.setDeviceId(deviceId);
|
||||
auth.setAuthType(request.getAuthType());
|
||||
auth.setRemark(request.getRemark());
|
||||
// 直接授权接口的角色由 API Key 决定:归属当前应用,遇到平台行/其他应用行拒绝覆盖
|
||||
auth.setSource(LockConstants.AUTH_SOURCE_OPENAPI);
|
||||
auth.setSourceAppCode(appCode);
|
||||
auth.setSourceBizNo(StringUtils.substring(StringUtils.trim(request.getBizNo()), 0, 64));
|
||||
try
|
||||
{
|
||||
auth.setBeginTime(parseTime(request.getBeginTime()));
|
||||
@@ -155,9 +166,10 @@ public class OpenAuthApiController
|
||||
*/
|
||||
@Anonymous
|
||||
@PostMapping("/{authId}/push")
|
||||
public AjaxResult push(@PathVariable Long authId)
|
||||
public AjaxResult push(@PathVariable Long authId, HttpServletRequest httpRequest)
|
||||
{
|
||||
if (authService.selectAuthById(authId) == null)
|
||||
// 跨应用一律按不存在处理,不暴露其他应用的数据
|
||||
if (authService.selectExternalAuthForApp(authId, resolveAppCode(httpRequest)) == null)
|
||||
{
|
||||
return AjaxResult.error("授权不存在,authId=" + authId);
|
||||
}
|
||||
@@ -170,9 +182,9 @@ public class OpenAuthApiController
|
||||
*/
|
||||
@Anonymous
|
||||
@PostMapping("/{authId}/clear")
|
||||
public AjaxResult clear(@PathVariable Long authId)
|
||||
public AjaxResult clear(@PathVariable Long authId, HttpServletRequest httpRequest)
|
||||
{
|
||||
if (authService.selectAuthById(authId) == null)
|
||||
if (authService.selectExternalAuthForApp(authId, resolveAppCode(httpRequest)) == null)
|
||||
{
|
||||
return AjaxResult.error("授权不存在,authId=" + authId);
|
||||
}
|
||||
@@ -185,9 +197,9 @@ public class OpenAuthApiController
|
||||
*/
|
||||
@Anonymous
|
||||
@DeleteMapping("/{authId}")
|
||||
public AjaxResult remove(@PathVariable Long authId)
|
||||
public AjaxResult remove(@PathVariable Long authId, HttpServletRequest httpRequest)
|
||||
{
|
||||
if (authService.selectAuthById(authId) == null)
|
||||
if (authService.selectExternalAuthForApp(authId, resolveAppCode(httpRequest)) == null)
|
||||
{
|
||||
return AjaxResult.error("授权不存在,authId=" + authId);
|
||||
}
|
||||
@@ -196,13 +208,33 @@ public class OpenAuthApiController
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询授权
|
||||
* 查询授权:只返回本应用下发的授权,不返回授权凭据
|
||||
*/
|
||||
@Anonymous
|
||||
@GetMapping("/list")
|
||||
public AjaxResult list(LockAuth auth)
|
||||
public AjaxResult list(LockAuth auth, HttpServletRequest httpRequest)
|
||||
{
|
||||
return AjaxResult.success(authService.selectAuthList(auth));
|
||||
String appCode = resolveAppCode(httpRequest);
|
||||
if (StringUtils.isEmpty(appCode))
|
||||
{
|
||||
// 没有应用身份时查询条件会退化为“全部外部授权”,必须直接拒绝
|
||||
return AjaxResult.error("应用身份缺失");
|
||||
}
|
||||
auth.setSource(LockConstants.AUTH_SOURCE_OPENAPI);
|
||||
auth.setSourceAppCode(appCode);
|
||||
auth.setAuthValue(null);
|
||||
if (StringUtils.isEmpty(auth.getSyncStatus()))
|
||||
{
|
||||
// 开放接口不套用平台页“默认隐藏已取消”的规则,调用方可据列表对账
|
||||
auth.setSyncStatus("ALL");
|
||||
}
|
||||
return AjaxResult.success(authService.selectExternalAuthList(auth));
|
||||
}
|
||||
|
||||
private String resolveAppCode(HttpServletRequest request)
|
||||
{
|
||||
Object value = request.getAttribute(OpenApiAuthFilter.ATTR_APP_CODE);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Date parseTime(String value) throws ParseException
|
||||
|
||||
+13
@@ -40,6 +40,9 @@ public class OpenAuthRequest implements Serializable
|
||||
/** 失效时间 yyyy-MM-dd HH:mm:ss,留空表示永久有效 */
|
||||
private String endTime;
|
||||
|
||||
/** 来源业务单号(可空,≤64,用于平台按单号追溯) */
|
||||
private String bizNo;
|
||||
|
||||
private String remark;
|
||||
|
||||
public String getPersonNo()
|
||||
@@ -112,6 +115,16 @@ public class OpenAuthRequest implements Serializable
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public String getBizNo()
|
||||
{
|
||||
return bizNo;
|
||||
}
|
||||
|
||||
public void setBizNo(String bizNo)
|
||||
{
|
||||
this.bizNo = bizNo;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
|
||||
@@ -25,6 +25,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<!-- 平台页面列表/详情额外带房间号;开放接口与住宿详情不查 room_no,继续用上面的基础 resultMap -->
|
||||
<resultMap type="LockAccessLog" id="LockAccessLogRoomResult" extends="LockAccessLogResult">
|
||||
<result property="roomNo" column="room_no" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectAccessLogVo">
|
||||
select log_id, device_id, device_sn, gateway_id, gateway_sn, group_id, person_no, person_name,
|
||||
card_no, open_type, open_result, open_dir, open_time, recv_time, seq, raw_params,
|
||||
|
||||
@@ -19,6 +19,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="beginTime" column="begin_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="authTime" column="auth_time" />
|
||||
<result property="source" column="source" />
|
||||
<result property="sourceAppCode" column="source_app_code" />
|
||||
<result property="sourceBizNo" column="source_biz_no" />
|
||||
<result property="sourceAppName" column="source_app_name" />
|
||||
<result property="stayNo" column="stay_no" />
|
||||
<result property="deviceName" column="device_name" />
|
||||
<result property="onlineStatus" column="online_status" />
|
||||
<result property="syncStatus" column="sync_status" />
|
||||
<result property="syncTime" column="sync_time" />
|
||||
<result property="syncMsg" column="sync_msg" />
|
||||
@@ -32,9 +39,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<resultMap type="LockAuthPerson" id="LockAuthPersonResult">
|
||||
<result property="personNo" column="person_no" />
|
||||
<result property="personName" column="person_name" />
|
||||
<result property="authTypes" column="auth_types" />
|
||||
<result property="authCount" column="auth_count" />
|
||||
<result property="deviceCount" column="device_count" />
|
||||
<result property="issuedCount" column="issued_count" />
|
||||
<result property="pendingCount" column="pending_count" />
|
||||
<result property="failedCount" column="failed_count" />
|
||||
<result property="beginTime" column="begin_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectAuthVo">
|
||||
select auth_id, auth_no, person_no, person_name, device_id, device_sn, gateway_id, group_id,
|
||||
auth_type, auth_value, auth_value_md5, begin_time, end_time, auth_time, sync_status,
|
||||
auth_type, auth_value, auth_value_md5, begin_time, end_time, auth_time,
|
||||
source, source_app_code, source_biz_no, sync_status,
|
||||
sync_time, sync_msg, retry_count, status, del_flag, create_by, create_time,
|
||||
update_by, update_time, remark from lock_auth
|
||||
</sql>
|
||||
@@ -67,6 +89,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="syncStatus != null and syncStatus != ''">
|
||||
and sync_status = #{syncStatus}
|
||||
</if>
|
||||
<if test="source != null and source != ''">
|
||||
and source = #{source}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
and status = #{status}
|
||||
</if>
|
||||
@@ -80,11 +105,206 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
order by auth_id desc
|
||||
</select>
|
||||
|
||||
<!--
|
||||
人员维度聚合:一人一行。已清除(4)的授权不再有设备侧权限,不计入;
|
||||
同步状态过滤作用于参与统计的行,便于「只看下发失败的授权」这类排查。
|
||||
-->
|
||||
<select id="selectPersonAuthList" parameterType="LockAuthPerson" resultMap="LockAuthPersonResult">
|
||||
select a.person_no,
|
||||
max(a.person_name) as person_name,
|
||||
count(1) as auth_count,
|
||||
count(distinct a.device_id) as device_count,
|
||||
sum(case when a.sync_status = '2' then 1 else 0 end) as issued_count,
|
||||
sum(case when a.sync_status is null or a.sync_status in ('0', '1', '6') then 1 else 0 end) as pending_count,
|
||||
sum(case when a.sync_status = '3' then 1 else 0 end) as failed_count,
|
||||
min(a.begin_time) as begin_time,
|
||||
max(a.end_time) as end_time,
|
||||
max(a.update_time) as update_time,
|
||||
group_concat(distinct a.auth_type order by a.auth_type separator ',') as auth_types
|
||||
from lock_auth a
|
||||
where a.del_flag = '0'
|
||||
and a.source = '1'
|
||||
and (a.sync_status is null or a.sync_status <> '4')
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (a.person_no like concat('%', #{keyword}, '%') or a.person_name like concat('%', #{keyword}, '%'))
|
||||
</if>
|
||||
<if test="deviceId != null">
|
||||
and a.device_id = #{deviceId}
|
||||
</if>
|
||||
<if test="groupId != null">
|
||||
and a.group_id = #{groupId}
|
||||
</if>
|
||||
<if test="authType != null and authType != ''">
|
||||
and a.auth_type = #{authType}
|
||||
</if>
|
||||
<if test="syncStatus != null and syncStatus != ''">
|
||||
and a.sync_status = #{syncStatus}
|
||||
</if>
|
||||
group by a.person_no
|
||||
order by max(a.update_time) desc, a.person_no
|
||||
</select>
|
||||
|
||||
<!-- 平台页签按人员操作只作用于平台授权,外部授权沿用外部页签的逐条操作 -->
|
||||
<select id="selectAuthIdsByPersonNos" resultType="Long">
|
||||
select auth_id from lock_auth
|
||||
where del_flag = '0'
|
||||
and source = '1'
|
||||
and person_no in
|
||||
<foreach collection="personNos" item="personNo" open="(" separator="," close=")">
|
||||
#{personNo}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="selectPushableAuthIdsByPersonNos" resultType="Long">
|
||||
select auth_id from lock_auth
|
||||
where del_flag = '0'
|
||||
and source = '1'
|
||||
and (sync_status is null or sync_status not in ('4', '6'))
|
||||
and person_no in
|
||||
<foreach collection="personNos" item="personNo" open="(" separator="," close=")">
|
||||
#{personNo}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<select id="selectAuthById" parameterType="Long" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where auth_id = #{authId}
|
||||
</select>
|
||||
|
||||
<!--
|
||||
授权明细:一行一条授权,带来源应用、门锁与关联入住单。
|
||||
不在 SQL 中查 auth_value,平台详情与开放接口列表都不回显凭据。
|
||||
入住单可能因换房/复用 auth_id 存在多条,取最近一条展示。
|
||||
-->
|
||||
<sql id="selectAuthDetailVo">
|
||||
select a.auth_id, a.auth_no, a.person_no, a.person_name, a.device_id, a.device_sn, a.gateway_id,
|
||||
a.group_id, a.auth_type, a.begin_time, a.end_time, a.auth_time,
|
||||
a.source, a.source_app_code, a.source_biz_no, a.sync_status, a.sync_time, a.sync_msg,
|
||||
a.retry_count, a.status, a.del_flag, a.create_by, a.create_time, a.update_by, a.update_time,
|
||||
a.remark, o.app_name as source_app_name, d.device_name, d.online_status, s.stay_no
|
||||
from lock_auth a
|
||||
left join lock_open_app o on o.app_code = a.source_app_code
|
||||
left join lock_device d on d.device_id = a.device_id
|
||||
left join lock_stay s on s.stay_id = (
|
||||
select max(s2.stay_id) from lock_stay s2 where s2.auth_id = a.auth_id
|
||||
)
|
||||
</sql>
|
||||
|
||||
<select id="selectExternalAuthList" parameterType="LockAuth" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthDetailVo"/>
|
||||
<where>
|
||||
a.del_flag = '0'
|
||||
and a.source = '2'
|
||||
<if test="sourceAppCode != null and sourceAppCode != ''">
|
||||
and a.source_app_code = #{sourceAppCode}
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (a.auth_no like concat('%', #{keyword}, '%')
|
||||
or a.person_no like concat('%', #{keyword}, '%')
|
||||
or a.person_name like concat('%', #{keyword}, '%'))
|
||||
</if>
|
||||
<if test="authNo != null and authNo != ''">
|
||||
and a.auth_no like concat('%', #{authNo}, '%')
|
||||
</if>
|
||||
<if test="personNo != null and personNo != ''">
|
||||
and a.person_no like concat('%', #{personNo}, '%')
|
||||
</if>
|
||||
<if test="personName != null and personName != ''">
|
||||
and a.person_name like concat('%', #{personName}, '%')
|
||||
</if>
|
||||
<if test="deviceId != null">
|
||||
and a.device_id = #{deviceId}
|
||||
</if>
|
||||
<if test="deviceSn != null and deviceSn != ''">
|
||||
and a.device_sn = #{deviceSn}
|
||||
</if>
|
||||
<if test="groupId != null">
|
||||
and a.group_id = #{groupId}
|
||||
</if>
|
||||
<if test="authType != null and authType != ''">
|
||||
and a.auth_type = #{authType}
|
||||
</if>
|
||||
<!-- ALL=全部状态;空=隐藏已取消;其余按状态精确过滤(4 即只看已取消) -->
|
||||
<choose>
|
||||
<when test="syncStatus == 'ALL'"></when>
|
||||
<when test="syncStatus != null and syncStatus != ''">
|
||||
and a.sync_status = #{syncStatus}
|
||||
</when>
|
||||
<otherwise>
|
||||
and (a.sync_status is null or a.sync_status != '4')
|
||||
</otherwise>
|
||||
</choose>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and date_format(a.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and date_format(a.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
order by a.auth_id desc
|
||||
</select>
|
||||
|
||||
<!-- 全部授权:平台与外部授权一并展示,一授权一行;默认不过滤已取消,便于逐条对账 -->
|
||||
<select id="selectAllAuthList" parameterType="LockAuth" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthDetailVo"/>
|
||||
<where>
|
||||
a.del_flag = '0'
|
||||
<if test="source != null and source != ''">
|
||||
and a.source = #{source}
|
||||
</if>
|
||||
<if test="sourceAppCode != null and sourceAppCode != ''">
|
||||
and a.source_app_code = #{sourceAppCode}
|
||||
</if>
|
||||
<if test="keyword != null and keyword != ''">
|
||||
and (a.auth_no like concat('%', #{keyword}, '%')
|
||||
or a.person_no like concat('%', #{keyword}, '%')
|
||||
or a.person_name like concat('%', #{keyword}, '%'))
|
||||
</if>
|
||||
<if test="authNo != null and authNo != ''">
|
||||
and a.auth_no like concat('%', #{authNo}, '%')
|
||||
</if>
|
||||
<if test="personNo != null and personNo != ''">
|
||||
and a.person_no like concat('%', #{personNo}, '%')
|
||||
</if>
|
||||
<if test="personName != null and personName != ''">
|
||||
and a.person_name like concat('%', #{personName}, '%')
|
||||
</if>
|
||||
<if test="deviceId != null">
|
||||
and a.device_id = #{deviceId}
|
||||
</if>
|
||||
<if test="deviceSn != null and deviceSn != ''">
|
||||
and a.device_sn = #{deviceSn}
|
||||
</if>
|
||||
<if test="groupId != null">
|
||||
and a.group_id = #{groupId}
|
||||
</if>
|
||||
<if test="authType != null and authType != ''">
|
||||
and a.auth_type = #{authType}
|
||||
</if>
|
||||
<if test="syncStatus != null and syncStatus != '' and syncStatus != 'ALL'">
|
||||
and a.sync_status = #{syncStatus}
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and date_format(a.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and date_format(a.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
order by a.auth_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectExternalAuthById" parameterType="Long" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthDetailVo"/>
|
||||
where a.auth_id = #{authId} and a.del_flag = '0' and a.source = '2'
|
||||
</select>
|
||||
|
||||
<select id="selectExternalAuthForApp" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthDetailVo"/>
|
||||
where a.auth_id = #{authId} and a.del_flag = '0' and a.source = '2'
|
||||
and a.source_app_code = #{appCode}
|
||||
</select>
|
||||
|
||||
<select id="selectAuthsByDeviceId" parameterType="Long" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where device_id = #{deviceId} and del_flag = '0'
|
||||
@@ -192,15 +412,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<insert id="insertAuth" parameterType="LockAuth" useGeneratedKeys="true" keyProperty="authId">
|
||||
insert into lock_auth (
|
||||
auth_no, person_no, person_name, device_id, device_sn, gateway_id, group_id, auth_type,
|
||||
auth_value, auth_value_md5, begin_time, end_time, auth_time, sync_status, retry_count,
|
||||
auth_value, auth_value_md5, begin_time, end_time, auth_time,
|
||||
source, source_app_code, source_biz_no, sync_status, retry_count,
|
||||
status, del_flag, create_by, create_time, remark
|
||||
) values (
|
||||
#{authNo}, #{personNo}, #{personName}, #{deviceId}, #{deviceSn}, #{gatewayId}, #{groupId},
|
||||
#{authType}, #{authValue}, #{authValueMd5}, #{beginTime}, #{endTime}, #{authTime},
|
||||
'0', 0, #{status}, '0', #{createBy}, CURRENT_TIMESTAMP, #{remark}
|
||||
#{source}, #{sourceAppCode}, #{sourceBizNo}, '0', 0, #{status}, '0', #{createBy},
|
||||
CURRENT_TIMESTAMP, #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 外部入住/换房覆盖平台行时接管归属;直接授权接口不允许走这里 -->
|
||||
<update id="updateAuthSource">
|
||||
update lock_auth
|
||||
set source = #{source},
|
||||
source_app_code = #{sourceAppCode},
|
||||
source_biz_no = #{sourceBizNo},
|
||||
update_by = #{updateBy},
|
||||
update_time = CURRENT_TIMESTAMP
|
||||
where auth_id = #{authId}
|
||||
</update>
|
||||
|
||||
<!-- 外部授权改期:显式写 end_time,允许把失效时间清空为 NULL(updateAuth 的 <if> 做不到) -->
|
||||
<update id="updateExternalAuth" parameterType="LockAuth">
|
||||
update lock_auth
|
||||
set begin_time = #{beginTime},
|
||||
end_time = #{endTime},
|
||||
remark = #{remark},
|
||||
update_by = #{updateBy},
|
||||
update_time = CURRENT_TIMESTAMP
|
||||
where auth_id = #{authId} and source = '2' and del_flag = '0'
|
||||
</update>
|
||||
|
||||
<update id="updateAuth" parameterType="LockAuth">
|
||||
update lock_auth
|
||||
<set>
|
||||
|
||||
@@ -99,6 +99,72 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and command_status in ('0', '1')
|
||||
</select>
|
||||
|
||||
<!-- 同一主题除自身外的在途命令:重发排队命令前确认网关没有别的 data/batch 在处理 -->
|
||||
<select id="countOtherInflightByTopic" resultType="int">
|
||||
select count(1) from lock_command
|
||||
where topic = #{topic}
|
||||
and command_status in ('0', '1')
|
||||
and command_id != #{commandId}
|
||||
</select>
|
||||
|
||||
<!-- 忙排队重发前只看已发送命令:同为待发送的其它命令由每轮"每主题只放行一条"控制 -->
|
||||
<select id="countOtherSentByTopic" resultType="int">
|
||||
select count(1) from lock_command
|
||||
where topic = #{topic}
|
||||
and command_status = '1'
|
||||
and command_id != #{commandId}
|
||||
</select>
|
||||
|
||||
<!-- 到期的忙排队命令,按先来先发 -->
|
||||
<select id="selectDeferredCommands" parameterType="java.util.Date" resultMap="LockCommandResult">
|
||||
<include refid="selectCommandVo"/>
|
||||
where command_status = '0'
|
||||
and next_retry_time is not null
|
||||
and next_retry_time <= #{now}
|
||||
order by command_id
|
||||
</select>
|
||||
|
||||
<!-- 同一授权已有在途命令时不再新建,避免命令重试与授权补偿并行下发同一张脸 -->
|
||||
<select id="countInflightAuthByBizId" resultType="int">
|
||||
select count(1) from lock_command
|
||||
where biz_id = #{bizId}
|
||||
and command_type in ('0011', '0012')
|
||||
and command_status in ('0', '1')
|
||||
</select>
|
||||
|
||||
<select id="countInflightByBizId" resultType="int">
|
||||
select count(1) from lock_command
|
||||
where command_type = #{commandType}
|
||||
and biz_id = #{bizId}
|
||||
and command_status in ('0', '1')
|
||||
</select>
|
||||
|
||||
<select id="selectLatestByBizId" resultMap="LockCommandResult">
|
||||
<include refid="selectCommandVo"/>
|
||||
where command_type = #{commandType}
|
||||
and biz_id = #{bizId}
|
||||
order by command_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 最近一次查询失败时回退到上次有数据的查询,避免离线重查把已有对账结果冲掉 -->
|
||||
<select id="selectLatestWithRowsByBizId" resultMap="LockCommandResult">
|
||||
<include refid="selectCommandVo"/>
|
||||
where command_type = #{commandType}
|
||||
and biz_id = #{bizId}
|
||||
and exists (select 1 from lock_device_auth a where a.command_no = lock_command.command_no)
|
||||
order by command_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectCommandsByNos" resultMap="LockCommandResult">
|
||||
<include refid="selectCommandVo"/>
|
||||
where command_no in
|
||||
<foreach collection="commandNos" item="commandNo" open="(" separator="," close=")">
|
||||
#{commandNo}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
<insert id="insertCommand" parameterType="LockCommand" useGeneratedKeys="true" keyProperty="commandId">
|
||||
insert into lock_command (
|
||||
command_no, command_type, target_type, target_id, target_code, biz_id, topic, payload,
|
||||
|
||||
@@ -120,6 +120,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where gateway_id = #{gatewayId}
|
||||
</update>
|
||||
|
||||
<update id="updateOnlineStatusOnly">
|
||||
update lock_gateway set online_status = #{onlineStatus}
|
||||
where gateway_id = #{gatewayId}
|
||||
</update>
|
||||
|
||||
<update id="updateSyncStatus">
|
||||
update lock_gateway set sync_status = #{syncStatus}, last_sync_time = CURRENT_TIMESTAMP
|
||||
<if test="syncMsg != null">, remark = #{syncMsg}</if>
|
||||
@@ -149,6 +154,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
where online_status = '0' and (last_online_time is null or last_online_time < #{offlineTime})
|
||||
</update>
|
||||
|
||||
<!--
|
||||
网关能转发门锁上报即证明在线:按归属行的最近上报时间刷新在线状态。
|
||||
只依赖 onoff_line 时,网关连接事件之后不再有消息,10 分钟后必然被误判离线。
|
||||
-->
|
||||
<update id="refreshOnlineByReport">
|
||||
update lock_gateway g
|
||||
join (
|
||||
select gateway_id, max(last_signal_time) as last_signal
|
||||
from lock_device_gateway
|
||||
where del_flag = '0' and gateway_id is not null
|
||||
group by gateway_id
|
||||
) r on r.gateway_id = g.gateway_id
|
||||
set g.online_status = '0',
|
||||
g.last_online_time = greatest(ifnull(g.last_online_time, r.last_signal), r.last_signal)
|
||||
where g.del_flag = '0'
|
||||
and r.last_signal >= #{onlineTime}
|
||||
and (g.online_status <> '0' or g.last_online_time is null or g.last_online_time < r.last_signal)
|
||||
</update>
|
||||
|
||||
<update id="deleteGatewayByIds" parameterType="Long">
|
||||
update lock_gateway set del_flag = '2' where gateway_id in
|
||||
<foreach collection="array" item="gatewayId" open="(" separator="," close=")">
|
||||
|
||||
@@ -312,6 +312,9 @@ create table lock_auth (
|
||||
begin_time datetime comment '生效开始时间',
|
||||
end_time datetime comment '生效结束时间(空表示永久有效)',
|
||||
auth_time datetime comment '授权操作时间',
|
||||
source char(1) default '1' comment '授权来源(1平台 2开放接口)',
|
||||
source_app_code varchar(32) default null comment '来源应用编码(source=2,lock_open_app.app_code)',
|
||||
source_biz_no varchar(64) default null comment '来源业务单号(可空;外部入住为 biz_no)',
|
||||
sync_status char(1) default '0' comment '下发同步状态(0待下发 1已下发 2下发成功 3下发失败 4已清除 6待清除;5 预留给 v2 的已过期)',
|
||||
sync_time datetime comment '最后下发/响应时间',
|
||||
sync_msg varchar(512) default null comment '下发结果说明',
|
||||
@@ -329,7 +332,8 @@ create table lock_auth (
|
||||
key idx_lock_auth_person (person_no),
|
||||
key idx_lock_auth_device (device_id),
|
||||
key idx_lock_auth_group (group_id),
|
||||
key idx_lock_auth_sync (sync_status)
|
||||
key idx_lock_auth_sync (sync_status),
|
||||
key idx_lock_auth_source (source, source_app_code)
|
||||
) engine=innodb comment = '授权数据表';
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user