fix(lock): 外部入住人员退房清理改为物理删除并强制每次入住传照片
- 退房/到期清理只针对外部接口自动建档人员(biz_system 非空、create_by=openapi:*、未绑平台账号、 无其它在住单与未清除授权),依次清理本地照片文件、人脸、各版本特征与人员档案; 人脸/特征/人员统一物理删除,lock_person_ref 保留以便设备回执回调与下次入住改指 - 对外入住每次必须传 photoUrl 或 photoBase64,缺参返回 40001,不再沿用历史照片 - lock_face 唯一键不含 del_flag:insertFace 命中历史行时走 reviveFace 复用, 修复重新建档撞 uk_lock_face_person_id 导致的 50002(Duplicate entry) - 同步设计稿 §2.4/§4.5/§12.1、Apifox 用例 C05/C22、项目看板 §4.35
This commit is contained in:
1 parent
271e4ca77c
commit
aae952ec13
19 files changed
+6160
No files matched your search
@@ -0,0 +1,211 @@
|
||||
package com.ruoyi.iot.lock;
|
||||
|
||||
/**
|
||||
* 门禁业务常量
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class LockConstants
|
||||
{
|
||||
/** 目标类型:网关 */
|
||||
public static final String TARGET_GATEWAY = "1";
|
||||
|
||||
/** 目标类型:门锁 */
|
||||
public static final String TARGET_DEVICE = "2";
|
||||
|
||||
/** 目标类型:分组 */
|
||||
public static final String TARGET_GROUP = "3";
|
||||
|
||||
/** 命令类型:绑定门锁 */
|
||||
public static final String CMD_BIND = "0001";
|
||||
|
||||
/** 命令类型:解绑门锁 */
|
||||
public static final String CMD_UNBIND = "0002";
|
||||
|
||||
/** 命令类型:同步时间 */
|
||||
public static final String CMD_SYNC_TIME = "0003";
|
||||
|
||||
/** 命令类型:远程开锁/关锁 */
|
||||
public static final String CMD_SWITCH = "0004";
|
||||
|
||||
/** 命令类型:修改网络参数 */
|
||||
public static final String CMD_NET_CFG = "0005";
|
||||
|
||||
/** 命令类型:修改LORA参数 */
|
||||
public static final String CMD_LORA_CFG = "0006";
|
||||
|
||||
/** 命令类型:修改MQTT参数 */
|
||||
public static final String CMD_MQTT_CFG = "0007";
|
||||
|
||||
/** 命令类型:保存配置 */
|
||||
public static final String CMD_SAVE = "0008";
|
||||
|
||||
/** 命令类型:重启网关 */
|
||||
public static final String CMD_REBOOT = "0009";
|
||||
|
||||
/** 命令类型:清除门锁 */
|
||||
public static final String CMD_CLEAR_DEVICE = "0010";
|
||||
|
||||
/** 命令类型:授权下发 */
|
||||
public static final String CMD_AUTH_PUSH = "0011";
|
||||
|
||||
/** 命令类型:清除授权 */
|
||||
public static final String CMD_AUTH_CLEAR = "0012";
|
||||
|
||||
/** 命令状态:待发送 */
|
||||
public static final String STATUS_PENDING = "0";
|
||||
|
||||
/** 命令状态:已发送 */
|
||||
public static final String STATUS_SENT = "1";
|
||||
|
||||
/** 命令状态:成功 */
|
||||
public static final String STATUS_SUCCESS = "2";
|
||||
|
||||
/** 命令状态:失败 */
|
||||
public static final String STATUS_FAIL = "3";
|
||||
|
||||
/** 命令状态:超时 */
|
||||
public static final String STATUS_TIMEOUT = "4";
|
||||
|
||||
/** 命令状态:已取消 */
|
||||
public static final String STATUS_CANCELED = "5";
|
||||
|
||||
/** 授权类型:人脸 */
|
||||
public static final String AUTH_FACE = "01";
|
||||
|
||||
/** 授权类型:密码 */
|
||||
public static final String AUTH_PASSWORD = "02";
|
||||
|
||||
/** 授权类型:指纹 */
|
||||
public static final String AUTH_FINGERPRINT = "03";
|
||||
|
||||
/** 授权类型:IC卡 */
|
||||
public static final String AUTH_IC_CARD = "04";
|
||||
|
||||
/** 同步状态:待下发 */
|
||||
public static final String SYNC_PENDING = "0";
|
||||
|
||||
/** 同步状态:已下发 */
|
||||
public static final String SYNC_SENT = "1";
|
||||
|
||||
/** 同步状态:下发成功 */
|
||||
public static final String SYNC_SUCCESS = "2";
|
||||
|
||||
/** 同步状态:下发失败 */
|
||||
public static final String SYNC_FAIL = "3";
|
||||
|
||||
/** 同步状态:已清除 */
|
||||
public static final String SYNC_CLEARED = "4";
|
||||
|
||||
/**
|
||||
* 同步状态:待清除(清除指令失败,等待补偿重发删除)。
|
||||
* <p>
|
||||
* 不能与 {@link #SYNC_FAIL} 混用:3 是「人脸下发失败」,补偿会重新 add;
|
||||
* 6 是「删除失败」,补偿必须继续 delete,否则退房/换房后会把人脸重新加回去。
|
||||
* 取 6 而不是 5:v2 基线规划 5 = 已过期,避免迁移冲突。
|
||||
*/
|
||||
public static final String SYNC_CLEAR_FAIL = "6";
|
||||
|
||||
/** locklist 下发状态:待下发 */
|
||||
public static final String BIND_PENDING = "0";
|
||||
|
||||
/** locklist 下发状态:已下发 */
|
||||
public static final String BIND_SENT = "1";
|
||||
|
||||
/** locklist 下发状态:下发失败 */
|
||||
public static final String BIND_FAIL = "2";
|
||||
|
||||
/** 在线:在线 */
|
||||
public static final String ONLINE = "0";
|
||||
|
||||
/** 在线:离线 */
|
||||
public static final String OFFLINE = "1";
|
||||
|
||||
/** 在线:未知 */
|
||||
public static final String ONLINE_UNKNOWN = "2";
|
||||
|
||||
/** 门锁状态:正常(status 字段,非在线状态) */
|
||||
public static final String DEVICE_STATUS_NORMAL = "0";
|
||||
|
||||
/**
|
||||
* 开放接口自动建档的 create_by 前缀。
|
||||
* <p>
|
||||
* 退房清理只针对以此为前缀的人员,人工建档人员无论是否办理过外部单都不动。
|
||||
*/
|
||||
public static final String CREATE_BY_OPENAPI = "openapi";
|
||||
|
||||
/** 人员状态:正常 */
|
||||
public static final String PERSON_STATUS_NORMAL = "0";
|
||||
|
||||
/** 人员状态:停用 */
|
||||
public static final String PERSON_STATUS_DISABLED = "1";
|
||||
|
||||
/** 授权状态:正常 */
|
||||
public static final String AUTH_STATUS_NORMAL = "0";
|
||||
|
||||
/** 授权状态:停用 */
|
||||
public static final String AUTH_STATUS_DISABLED = "1";
|
||||
|
||||
/** 特征提取状态:未提取 */
|
||||
public static final String EXTRACT_PENDING = "0";
|
||||
|
||||
/** 特征提取状态:成功 */
|
||||
public static final String EXTRACT_SUCCESS = "1";
|
||||
|
||||
/** 特征提取状态:失败 */
|
||||
public static final String EXTRACT_FAIL = "2";
|
||||
|
||||
/** 配置类型:MQTT服务器 */
|
||||
public static final String CONFIG_MQTT = "01";
|
||||
|
||||
/** 配置类型:人脸特征服务 */
|
||||
public static final String CONFIG_FACE = "02";
|
||||
|
||||
/** 配置类型:平台参数 */
|
||||
public static final String CONFIG_PLATFORM = "03";
|
||||
|
||||
/** 报文方向:上行 */
|
||||
public static final String DIRECTION_UP = "1";
|
||||
|
||||
/** 报文方向:下行 */
|
||||
public static final String DIRECTION_DOWN = "2";
|
||||
|
||||
/** 报文处理状态:成功 */
|
||||
public static final String HANDLE_SUCCESS = "1";
|
||||
|
||||
/** 报文处理状态:失败 */
|
||||
public static final String HANDLE_FAIL = "2";
|
||||
|
||||
/** 报文处理状态:已忽略 */
|
||||
public static final String HANDLE_IGNORE = "3";
|
||||
|
||||
/** 设备响应结果码:执行成功(终态) */
|
||||
public static final String RESP_CODE_SUCCESS = "0";
|
||||
|
||||
/**
|
||||
* 设备响应结果码:网关已接收并转发(中间态)
|
||||
* <p>
|
||||
* 门锁类下发会收到两次响应:网关先回执(本码),门锁执行完再回最终结果码。
|
||||
* 中间态只留痕,不能据此判定命令完成,否则会覆盖门锁的真实结果。
|
||||
*/
|
||||
public static final String RESP_CODE_GATEWAY_RECEIVED = "10";
|
||||
|
||||
/** 回执标记:未收到 */
|
||||
public static final String ACK_NO = "0";
|
||||
|
||||
/** 回执标记:已收到 */
|
||||
public static final String ACK_YES = "1";
|
||||
|
||||
/** 门锁回执结果:失败 */
|
||||
public static final String DEVICE_RESULT_FAIL = "0";
|
||||
|
||||
/** 门锁回执结果:成功 */
|
||||
public static final String DEVICE_RESULT_SUCCESS = "1";
|
||||
|
||||
/** 开门结果:成功 */
|
||||
public static final String OPEN_RESULT_SUCCESS = "1";
|
||||
|
||||
private LockConstants()
|
||||
{
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.ruoyi.iot.lock.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.iot.lock.domain.LockAuth;
|
||||
|
||||
/**
|
||||
* 授权数据 数据层
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface LockAuthMapper
|
||||
{
|
||||
List<LockAuth> selectAuthList(LockAuth auth);
|
||||
|
||||
LockAuth selectAuthById(Long authId);
|
||||
|
||||
List<LockAuth> selectAuthsByDeviceId(Long deviceId);
|
||||
|
||||
List<LockAuth> selectAuthsByPersonNo(String personNo);
|
||||
|
||||
/** 人员未撤销的授权数,删除人员前的引用校验 */
|
||||
int countAuthsByPersonNo(String personNo);
|
||||
|
||||
/**
|
||||
* 人员除指定授权外未清除的授权数。
|
||||
* <p>
|
||||
* 已清除(4)的授权不再有设备侧权限,不计入;下发失败(3)与清除失败(6)都保留,
|
||||
* 前者可能仍能补偿重发,后者说明人脸还在设备上。
|
||||
*/
|
||||
int countActiveAuthsByPersonNo(@Param("personNo") String personNo, @Param("excludeAuthId") Long excludeAuthId);
|
||||
|
||||
/** 按分组查询待下发或下发失败的授权,用于补偿下发 */
|
||||
List<LockAuth> selectPendingAuths(@Param("groupId") Long groupId);
|
||||
|
||||
/** 待清除授权(清除指令失败),补偿必须重发删除而不是人脸下发 */
|
||||
List<LockAuth> selectPendingClearAuths();
|
||||
|
||||
/** 人员人脸授权涉及的门锁算法版本集合(去重),批量提取用 */
|
||||
List<String> selectFaceAlgoVersionsByPersonNo(String personNo);
|
||||
|
||||
/** 指定版本下待下发/下发失败的人脸授权,特征就绪后自动补发 */
|
||||
List<LockAuth> selectFaceAuthsForRepush(@Param("personNo") String personNo,
|
||||
@Param("algoVersion") String algoVersion);
|
||||
|
||||
int insertAuth(LockAuth auth);
|
||||
|
||||
/**
|
||||
* 按唯一键 (person_no, device_id, auth_type) 查授权。
|
||||
* <p>
|
||||
* 唯一键不含 del_flag,逻辑删除的行仍占用键值,所以这里不过滤 del_flag。
|
||||
*/
|
||||
LockAuth selectAuthByUniqueKey(@Param("personNo") String personNo, @Param("deviceId") Long deviceId,
|
||||
@Param("authType") String authType);
|
||||
|
||||
/** 复用已存在的授权行(含已逻辑删除),写入新的凭据并重置为待下发 */
|
||||
int reviveAuth(LockAuth auth);
|
||||
|
||||
int updateAuth(LockAuth auth);
|
||||
|
||||
int updateSyncStatus(@Param("authId") Long authId, @Param("syncStatus") String syncStatus,
|
||||
@Param("syncMsg") String syncMsg);
|
||||
|
||||
/** 门锁算法版本变更:把该锁上未撤销的人脸授权置为下发失败,等待按新版本重提后补发 */
|
||||
int markFaceAuthsForReextract(@Param("deviceId") Long deviceId, @Param("syncMsg") String syncMsg);
|
||||
|
||||
int deleteAuthByIds(Long[] authIds);
|
||||
|
||||
int deleteAuthByDeviceId(Long deviceId);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.iot.lock.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.iot.lock.domain.LockFace;
|
||||
|
||||
/**
|
||||
* 人脸数据 数据层
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface LockFaceMapper
|
||||
{
|
||||
/** 列表查询不返回 feature_data,避免大字段传输 */
|
||||
List<LockFace> selectFaceList(LockFace face);
|
||||
|
||||
LockFace selectFaceById(Long faceId);
|
||||
|
||||
LockFace selectFaceByPersonNo(String personNo);
|
||||
|
||||
/** 人员已登记的人脸数,删除人员前的引用校验 */
|
||||
int countFaceByPersonId(Long personId);
|
||||
|
||||
/** 引用同一照片文件的在用人脸数,调用方复用同一 photoUrl 时不能直接删文件 */
|
||||
int countFaceByPhotoUrl(String photoUrl);
|
||||
|
||||
/** 重新建档时查历史人脸行(含已逻辑删除):唯一键不含 del_flag,直接插会撞键 */
|
||||
LockFace selectReusableFace(@Param("personId") Long personId, @Param("personNo") String personNo);
|
||||
|
||||
/** 复用历史行:重置为在用并清空上一次的提取结果 */
|
||||
int reviveFace(LockFace face);
|
||||
|
||||
int insertFace(LockFace face);
|
||||
|
||||
int updateFace(LockFace face);
|
||||
|
||||
int updateExtractResult(LockFace face);
|
||||
|
||||
int deleteFaceByIds(Long[] faceIds);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.iot.lock.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.iot.lock.domain.LockPersonRef;
|
||||
|
||||
/**
|
||||
* 外部人员标识映射 数据层
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface LockPersonRefMapper
|
||||
{
|
||||
/** 按 (应用, 外部标识) 取映射,不存在返回 null */
|
||||
LockPersonRef selectByAppAndExternalId(@Param("appCode") String appCode,
|
||||
@Param("externalId") String externalId);
|
||||
|
||||
/** 按 (应用, 平台人员编号) 反查外部标识(回调报文的 externalPersonId) */
|
||||
LockPersonRef selectByAppAndPersonNo(@Param("appCode") String appCode, @Param("personNo") String personNo);
|
||||
|
||||
/** 新增映射(撞唯一键时由业务层捕获后回查) */
|
||||
int insertRef(LockPersonRef ref);
|
||||
|
||||
/**
|
||||
* 把映射改指到新人员(外部人员退房清理后重新入住)。
|
||||
* <p>
|
||||
* 带旧人员编号做条件更新:并发下只有先改成功的请求生效,另一方回查映射后
|
||||
* 改用现有人员,与首次建档撞键的处理保持一致。
|
||||
*/
|
||||
int rebindPerson(@Param("refId") Long refId, @Param("oldPersonNo") String oldPersonNo,
|
||||
@Param("personId") Long personId, @Param("personNo") String personNo);
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package com.ruoyi.iot.lock.mapper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.ruoyi.iot.lock.domain.LockStay;
|
||||
|
||||
/**
|
||||
* 入住单 数据层
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface LockStayMapper
|
||||
{
|
||||
List<LockStay> selectStayList(LockStay stay);
|
||||
|
||||
LockStay selectStayById(Long stayId);
|
||||
|
||||
/** 某人在住订单(一人多房,返回列表) */
|
||||
List<LockStay> selectActiveStaysByPersonNo(String personNo);
|
||||
|
||||
/** 某人未结束的入住单数(退房清理人员前排除当前单的占用校验) */
|
||||
int countActiveStaysByPersonNo(@Param("personNo") String personNo, @Param("excludeStayId") Long excludeStayId);
|
||||
|
||||
/** 房间入住历史 */
|
||||
List<LockStay> selectStaysByRoomId(Long roomId);
|
||||
|
||||
/** 房间当前在住单(一房可多人),按入住先后倒序 */
|
||||
List<LockStay> selectActiveStaysByRoomId(Long roomId);
|
||||
|
||||
/** 按业务系统单号取入住单(对外接口幂等与定位,跨应用查不到) */
|
||||
LockStay selectStayByBiz(@Param("bizSystem") String bizSystem, @Param("bizNo") String bizNo);
|
||||
|
||||
/** 某人在某房间、某业务系统下的在住单(对外退房/续住按人员+房间定位) */
|
||||
LockStay selectActiveStayByPersonAndRoom(@Param("personNo") String personNo, @Param("roomId") Long roomId,
|
||||
@Param("bizSystem") String bizSystem);
|
||||
|
||||
/** 某人在某房间、某业务系统下最近一条入住单(不限状态,用于已退房时幂等返回) */
|
||||
LockStay selectLatestStayByPersonAndRoom(@Param("personNo") String personNo, @Param("roomId") Long roomId,
|
||||
@Param("bizSystem") String bizSystem);
|
||||
|
||||
/** 授权对应的入住单(回调事件按 auth_id 反查业务单号与房号;一对一,取最新一条) */
|
||||
LockStay selectStayByAuthId(Long authId);
|
||||
|
||||
/** 门锁上的在住单数(整锁清空人脸前的安全校验) */
|
||||
int countActiveStaysByDeviceId(Long deviceId);
|
||||
|
||||
/** 全平台在住单数(修改系统时间前的风险提示) */
|
||||
int countActiveStays();
|
||||
|
||||
/** 某业务系统的入住单数(删除外部应用前的占用校验) */
|
||||
int countStaysByBizSystem(String bizSystem);
|
||||
|
||||
/** 已到预计退房时间但仍为在住的入住单(到期任务扫描) */
|
||||
List<LockStay> selectExpiredStays(@Param("endTime") Date endTime, @Param("limit") int limit);
|
||||
|
||||
/** 到期:在住 → 已退房,退房时间取计划结束时间 */
|
||||
int updateExpire(Long stayId);
|
||||
|
||||
int insertStay(LockStay stay);
|
||||
|
||||
/** 退房:置已退房并记录退房时间 */
|
||||
int updateCheckout(@Param("stayId") Long stayId, @Param("checkoutTime") Date checkoutTime);
|
||||
|
||||
/** 换房:旧单置已换房并记录时间 */
|
||||
int updateChanged(@Param("stayId") Long stayId, @Param("checkoutTime") Date checkoutTime);
|
||||
|
||||
/** 续住:更新预计退房时间 */
|
||||
int updateEndTime(@Param("stayId") Long stayId, @Param("endTime") Date endTime);
|
||||
|
||||
/** 授权回执回写入住单的人脸授权状态 */
|
||||
int updateAuthStatusByAuthId(@Param("authId") Long authId, @Param("authStatus") String authStatus,
|
||||
@Param("authMsg") String authMsg);
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package com.ruoyi.iot.lock.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.iot.lock.domain.LockFace;
|
||||
import com.ruoyi.iot.lock.domain.LockFaceFeature;
|
||||
|
||||
/**
|
||||
* 人脸特征服务
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface ILockFaceService
|
||||
{
|
||||
List<LockFace> selectFaceList(LockFace face);
|
||||
|
||||
LockFace selectFaceById(Long faceId);
|
||||
|
||||
LockFace selectFaceByPersonNo(String personNo);
|
||||
|
||||
/**
|
||||
* 删除平台本地保存的人脸照片文件。
|
||||
* <p>
|
||||
* 只处理 {@code /profile/...} 相对地址(上传与 base64 落盘的照片),且限定在
|
||||
* profile 目录内;外部 http(s) 地址由业务系统自己保管,平台不删除。
|
||||
*
|
||||
* @param photoUrl 人脸记录中的照片地址
|
||||
* @return 删除的文件数(不存在或非本地地址返回 0)
|
||||
*/
|
||||
int deleteLocalPhotoFile(String photoUrl);
|
||||
|
||||
/**
|
||||
* 取人员在指定算法版本下已提取成功的特征
|
||||
*
|
||||
* @param personNo 人员编号
|
||||
* @param algoVersion 算法版本(门锁 face_algo_version)
|
||||
* @return 无可用特征时返回 null
|
||||
*/
|
||||
LockFaceFeature selectFeature(String personNo, String algoVersion);
|
||||
|
||||
/** 人脸已提取的算法版本列表(不含特征值) */
|
||||
List<LockFaceFeature> selectFeaturesByFaceId(Long faceId);
|
||||
|
||||
int insertFace(LockFace face);
|
||||
|
||||
int updateFace(LockFace face);
|
||||
|
||||
int deleteFaceByIds(Long[] faceIds);
|
||||
|
||||
/**
|
||||
* 触发特征提取
|
||||
*
|
||||
* @param faceId 人脸ID
|
||||
* @param deviceId 目标门锁ID(用于取该锁维护的 algorithm_version),为空时按该人员的授权自动推断
|
||||
* @return 提取后的特征(base64),失败返回 null
|
||||
*/
|
||||
String extract(Long faceId, Long deviceId);
|
||||
|
||||
/**
|
||||
* 批量重提取
|
||||
*
|
||||
* @param faceIds 人脸ID数组
|
||||
* @param deviceId 目标门锁ID,为空时按各自授权自动推断
|
||||
* @return 成功条数
|
||||
*/
|
||||
int batchExtract(Long[] faceIds, Long deviceId);
|
||||
}
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
package com.ruoyi.iot.lock.service.impl;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.sign.Md5Utils;
|
||||
import com.ruoyi.iot.lock.LockConstants;
|
||||
import com.ruoyi.iot.lock.domain.LockAuth;
|
||||
import com.ruoyi.iot.lock.domain.LockDevice;
|
||||
import com.ruoyi.iot.lock.domain.LockFace;
|
||||
import com.ruoyi.iot.lock.domain.LockFaceFeature;
|
||||
import com.ruoyi.iot.lock.mapper.LockAuthMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockDeviceMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockFaceFeatureMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockFaceMapper;
|
||||
import com.ruoyi.iot.lock.remote.FaceExtractClient;
|
||||
import com.ruoyi.iot.lock.remote.FaceExtractResult;
|
||||
import com.ruoyi.iot.lock.service.ILockAuthService;
|
||||
import com.ruoyi.iot.lock.service.ILockFaceService;
|
||||
import com.ruoyi.iot.person.domain.LockPerson;
|
||||
import com.ruoyi.iot.person.mapper.LockPersonMapper;
|
||||
|
||||
/**
|
||||
* 人脸特征服务实现
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class LockFaceServiceImpl implements ILockFaceService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(LockFaceServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private LockFaceMapper faceMapper;
|
||||
|
||||
@Autowired
|
||||
private LockFaceFeatureMapper featureMapper;
|
||||
|
||||
@Autowired
|
||||
private LockDeviceMapper deviceMapper;
|
||||
|
||||
@Autowired
|
||||
private LockAuthMapper authMapper;
|
||||
|
||||
@Autowired
|
||||
private LockPersonMapper personMapper;
|
||||
|
||||
@Autowired
|
||||
private FaceExtractClient faceExtractClient;
|
||||
|
||||
@Autowired
|
||||
private ILockAuthService authService;
|
||||
|
||||
@Override
|
||||
public List<LockFace> selectFaceList(LockFace face)
|
||||
{
|
||||
return faceMapper.selectFaceList(face);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockFace selectFaceById(Long faceId)
|
||||
{
|
||||
return faceMapper.selectFaceById(faceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockFace selectFaceByPersonNo(String personNo)
|
||||
{
|
||||
return faceMapper.selectFaceByPersonNo(personNo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteLocalPhotoFile(String photoUrl)
|
||||
{
|
||||
String url = StringUtils.trim(photoUrl);
|
||||
if (StringUtils.isEmpty(url) || !url.startsWith(Constants.RESOURCE_PREFIX))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
String relative = StringUtils.substringAfter(url, Constants.RESOURCE_PREFIX);
|
||||
try
|
||||
{
|
||||
// 调用方可能把同一张照片给多人用,还有其它人脸引用时只删记录不删文件
|
||||
if (faceMapper.countFaceByPhotoUrl(url) > 1)
|
||||
{
|
||||
log.debug("照片被多个人脸引用,保留文件, photoUrl={}", url);
|
||||
return 0;
|
||||
}
|
||||
Path profile = Paths.get(RuoYiConfig.getProfile()).toAbsolutePath().normalize();
|
||||
Path target = profile.resolve(relative.startsWith("/") ? relative.substring(1) : relative).normalize();
|
||||
// photo_url 来自外部传入,必须确认落在 profile 目录内再删,避免路径穿越
|
||||
if (!target.startsWith(profile) || !Files.isRegularFile(target))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
Files.delete(target);
|
||||
return 1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("删除人脸照片文件失败, photoUrl={}, msg={}", url, e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockFaceFeature selectFeature(String personNo, String algoVersion)
|
||||
{
|
||||
if (StringUtils.isEmpty(personNo) || StringUtils.isEmpty(algoVersion))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return featureMapper.selectUsableFeature(personNo, algoVersion.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockFaceFeature> selectFeaturesByFaceId(Long faceId)
|
||||
{
|
||||
return featureMapper.selectFeaturesByFaceId(faceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertFace(LockFace face)
|
||||
{
|
||||
// 人脸必须挂在人员主数据上:人员不存在时拒绝建档,避免再出现「人脸表自带人员信息」
|
||||
LockPerson person = resolvePerson(face.getPersonNo());
|
||||
face.setPersonId(person.getPersonId());
|
||||
face.setPersonNo(person.getPersonNo());
|
||||
face.setExtractStatus(LockConstants.EXTRACT_PENDING);
|
||||
// 唯一键不含 del_flag:退房清理、人工删人脸留下的历史行会挡住重新建档,命中就复用该行
|
||||
LockFace history = faceMapper.selectReusableFace(person.getPersonId(), person.getPersonNo());
|
||||
if (history != null)
|
||||
{
|
||||
face.setFaceId(history.getFaceId());
|
||||
face.setUpdateBy(face.getCreateBy());
|
||||
if (StringUtils.isEmpty(face.getStatus()))
|
||||
{
|
||||
face.setStatus("0");
|
||||
}
|
||||
faceMapper.reviveFace(face);
|
||||
return 1;
|
||||
}
|
||||
return faceMapper.insertFace(face);
|
||||
}
|
||||
|
||||
private LockPerson resolvePerson(String personNo)
|
||||
{
|
||||
if (StringUtils.isEmpty(StringUtils.trim(personNo)))
|
||||
{
|
||||
throw new IllegalArgumentException("人员编号不能为空");
|
||||
}
|
||||
LockPerson person = personMapper.selectPersonByNo(StringUtils.trim(personNo));
|
||||
if (person == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人员不存在,请先建立人员档案,人员编号=" + personNo);
|
||||
}
|
||||
return person;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateFace(LockFace face)
|
||||
{
|
||||
return faceMapper.updateFace(face);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int deleteFaceByIds(Long[] faceIds)
|
||||
{
|
||||
// 人脸与各版本特征都是生物识别数据,直接物理删除,不留历史行
|
||||
featureMapper.deleteFeaturesByFaceIds(faceIds);
|
||||
return faceMapper.deleteFaceByIds(faceIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String extract(Long faceId, Long deviceId)
|
||||
{
|
||||
LockFace face = faceMapper.selectFaceById(faceId);
|
||||
if (face == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人脸数据不存在");
|
||||
}
|
||||
// algorithm_version 由门锁维护,优先取指定门锁,其次按该人员的授权推断
|
||||
String algoVersion = resolveAlgoVersion(face.getPersonNo(), deviceId);
|
||||
if (StringUtils.isEmpty(algoVersion))
|
||||
{
|
||||
// 特征按版本分别存储,没有版本就没有可下发的目标,直接失败而不是落一份无版本特征
|
||||
log.warn("未获取到算法版本(algorithm_version), faceId={}, deviceId={}", faceId, deviceId);
|
||||
markExtractFail(face, null, "未获取到门锁算法版本(lock_device.face_algo_version),无法按版本提取特征");
|
||||
return null;
|
||||
}
|
||||
return extractForVersion(face, algoVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按指定算法版本提取并落库
|
||||
* <p>
|
||||
* 同一张人脸的不同版本各占 lock_face_feature 一行,互不覆盖;
|
||||
* lock_face 上的特征列同步为最近一次成功结果,保持旧页面与开放接口兼容。
|
||||
*/
|
||||
private String extractForVersion(LockFace face, String algoVersion)
|
||||
{
|
||||
if (StringUtils.isEmpty(face.getPhotoUrl()))
|
||||
{
|
||||
markExtractFail(face, algoVersion, "未上传人脸图片");
|
||||
return null;
|
||||
}
|
||||
// photoUrl 存的是图片地址,特征提取服务按文件接收,这里读取图片内容
|
||||
byte[] imageBytes = loadImageBytes(face.getPhotoUrl());
|
||||
if (imageBytes == null || imageBytes.length == 0)
|
||||
{
|
||||
markExtractFail(face, algoVersion, "人脸图片读取失败,请确认图片地址可访问");
|
||||
log.warn("人脸图片读取失败, faceId={}, photoUrl={}", face.getFaceId(), face.getPhotoUrl());
|
||||
return null;
|
||||
}
|
||||
FaceExtractResult result = faceExtractClient.extract(imageBytes, "face_" + face.getFaceId() + ".jpg",
|
||||
algoVersion);
|
||||
if (!result.isSuccess())
|
||||
{
|
||||
markExtractFail(face, algoVersion, StringUtils.substring(result.getErrorMessage(), 0, 500));
|
||||
log.warn("人脸特征提取失败, faceId={}, algoVersion={}, reason={}", face.getFaceId(), algoVersion,
|
||||
result.getErrorMessage());
|
||||
return null;
|
||||
}
|
||||
String feature = result.getFeature();
|
||||
String featureMd5 = Md5Utils.hash(feature);
|
||||
|
||||
LockFaceFeature row = new LockFaceFeature();
|
||||
row.setFaceId(face.getFaceId());
|
||||
row.setPersonId(face.getPersonId());
|
||||
row.setPersonNo(face.getPersonNo());
|
||||
row.setAlgoVersion(algoVersion);
|
||||
row.setFeatureData(feature);
|
||||
row.setFeatureMd5(featureMd5);
|
||||
row.setFeatureSize(feature.length());
|
||||
row.setExtractStatus(LockConstants.EXTRACT_SUCCESS);
|
||||
row.setExtractTime(new Date());
|
||||
featureMapper.upsertFeature(row);
|
||||
|
||||
LockFace update = new LockFace();
|
||||
update.setFaceId(face.getFaceId());
|
||||
update.setExtractStatus(LockConstants.EXTRACT_SUCCESS);
|
||||
update.setExtractMsg(null);
|
||||
update.setFeatureData(feature);
|
||||
update.setFeatureMd5(featureMd5);
|
||||
update.setFeatureSize(feature.length());
|
||||
update.setFeatureAlgoVersion(algoVersion);
|
||||
faceMapper.updateExtractResult(update);
|
||||
|
||||
// 新版本特征就绪后,自动补发此前因版本不匹配或等待重提而失败的授权
|
||||
repushPendingFaceAuths(face.getPersonNo(), algoVersion);
|
||||
return feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取失败落库
|
||||
* <p>
|
||||
* 该版本已有可用特征时不降级:重复提取失败不应让原本可下发的特征失效,
|
||||
* 只把本次失败原因写到 lock_face 镜像供页面提示。
|
||||
*/
|
||||
private void markExtractFail(LockFace face, String algoVersion, String message)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(algoVersion))
|
||||
{
|
||||
LockFaceFeature existing = featureMapper.selectFeatureByFaceAndVersion(face.getFaceId(), algoVersion);
|
||||
boolean hasUsable = existing != null
|
||||
&& LockConstants.EXTRACT_SUCCESS.equals(existing.getExtractStatus())
|
||||
&& StringUtils.isNotEmpty(existing.getFeatureData());
|
||||
if (!hasUsable)
|
||||
{
|
||||
LockFaceFeature row = new LockFaceFeature();
|
||||
row.setFaceId(face.getFaceId());
|
||||
row.setPersonId(face.getPersonId());
|
||||
row.setPersonNo(face.getPersonNo());
|
||||
row.setAlgoVersion(algoVersion);
|
||||
row.setExtractStatus(LockConstants.EXTRACT_FAIL);
|
||||
row.setExtractTime(new Date());
|
||||
row.setExtractMsg(message);
|
||||
featureMapper.upsertFeature(row);
|
||||
}
|
||||
}
|
||||
LockFace update = new LockFace();
|
||||
update.setFaceId(face.getFaceId());
|
||||
update.setExtractStatus(LockConstants.EXTRACT_FAIL);
|
||||
update.setExtractMsg(message);
|
||||
faceMapper.updateExtractResult(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* 特征就绪后补发该版本下待下发/失败的授权
|
||||
*/
|
||||
private void repushPendingFaceAuths(String personNo, String algoVersion)
|
||||
{
|
||||
List<LockAuth> auths = authMapper.selectFaceAuthsForRepush(personNo, algoVersion);
|
||||
for (LockAuth auth : auths)
|
||||
{
|
||||
try
|
||||
{
|
||||
authService.push(auth.getAuthId());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("特征就绪后重发授权失败, authId={}, msg={}", auth.getAuthId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析特征提取所需的 algorithm_version
|
||||
* <p>
|
||||
* 该参数在每把智能门锁上维护(lock_device.face_algo_version):
|
||||
* 优先使用指定的目标门锁,未指定时回退到该人员第一条授权对应的门锁。
|
||||
*
|
||||
* @param personNo 人员编号
|
||||
* @param deviceId 目标门锁ID,可为空
|
||||
* @return 算法版本,取不到返回 null
|
||||
*/
|
||||
private String resolveAlgoVersion(String personNo, Long deviceId)
|
||||
{
|
||||
LockDevice device = null;
|
||||
if (deviceId != null)
|
||||
{
|
||||
device = deviceMapper.selectDeviceById(deviceId);
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(personNo))
|
||||
{
|
||||
List<LockAuth> auths = authMapper.selectAuthsByPersonNo(personNo);
|
||||
if (auths != null && !auths.isEmpty() && auths.get(0).getDeviceId() != null)
|
||||
{
|
||||
device = deviceMapper.selectDeviceById(auths.get(0).getDeviceId());
|
||||
}
|
||||
}
|
||||
if (device == null || StringUtils.isEmpty(device.getFaceAlgoVersion()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return device.getFaceAlgoVersion().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取人脸图片内容
|
||||
* <p>
|
||||
* photoUrl 可能是 http(s) 外链,也可能是若依上传接口返回的 /profile/... 相对地址
|
||||
* (实际文件位于 RuoYiConfig.getProfile() 对应的目录)。
|
||||
*/
|
||||
private byte[] loadImageBytes(String photoUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (photoUrl.startsWith("http"))
|
||||
{
|
||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(photoUrl))
|
||||
.timeout(Duration.ofSeconds(30)).GET().build();
|
||||
HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||
if (response.statusCode() != 200)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
String profile = RuoYiConfig.getProfile();
|
||||
String relative = StringUtils.substringAfter(photoUrl, Constants.RESOURCE_PREFIX);
|
||||
Path path = Paths.get(profile + relative);
|
||||
if (!Files.exists(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Files.readAllBytes(path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("人脸图片读取失败, photoUrl={}", photoUrl, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchExtract(Long[] faceIds, Long deviceId)
|
||||
{
|
||||
int success = 0;
|
||||
for (Long faceId : faceIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
LockFace face = faceMapper.selectFaceById(faceId);
|
||||
if (face == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 未指定门锁时按该人员已授权门锁的版本集合逐个提取,一次补齐多个版本
|
||||
List<String> versions = resolveRequiredVersions(face.getPersonNo(), deviceId);
|
||||
if (versions.isEmpty())
|
||||
{
|
||||
// 没有目标版本时仍走一次 extract,让失败原因落到 lock_face
|
||||
if (extract(faceId, deviceId) != null)
|
||||
{
|
||||
success++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
boolean allOk = true;
|
||||
for (String version : versions)
|
||||
{
|
||||
if (extractForVersion(face, version) == null)
|
||||
{
|
||||
allOk = false;
|
||||
}
|
||||
}
|
||||
if (allOk)
|
||||
{
|
||||
success++;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("批量特征提取异常, faceId={}, msg={}", faceId, e.getMessage());
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析批量提取需要覆盖的算法版本
|
||||
*
|
||||
* @param deviceId 指定门锁时只提该锁版本;为空时取该人员人脸授权门锁的版本集合
|
||||
*/
|
||||
private List<String> resolveRequiredVersions(String personNo, Long deviceId)
|
||||
{
|
||||
if (deviceId != null)
|
||||
{
|
||||
LockDevice device = deviceMapper.selectDeviceById(deviceId);
|
||||
String version = device == null ? null : StringUtils.trim(device.getFaceAlgoVersion());
|
||||
return StringUtils.isEmpty(version) ? Collections.emptyList() : Collections.singletonList(version);
|
||||
}
|
||||
List<String> versions = authMapper.selectFaceAlgoVersionsByPersonNo(personNo);
|
||||
if (versions != null && !versions.isEmpty())
|
||||
{
|
||||
return versions;
|
||||
}
|
||||
String fallback = resolveAlgoVersion(personNo, null);
|
||||
return StringUtils.isEmpty(fallback) ? Collections.emptyList() : Collections.singletonList(fallback);
|
||||
}
|
||||
}
|
||||
+831
@@ -0,0 +1,831 @@
|
||||
package com.ruoyi.iot.lock.service.impl;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.iot.lock.LockConstants;
|
||||
import com.ruoyi.iot.lock.domain.LockAccessLog;
|
||||
import com.ruoyi.iot.lock.domain.LockAuth;
|
||||
import com.ruoyi.iot.lock.domain.LockDevice;
|
||||
import com.ruoyi.iot.lock.domain.LockFace;
|
||||
import com.ruoyi.iot.lock.domain.LockFaceFeature;
|
||||
import com.ruoyi.iot.lock.domain.LockPersonRef;
|
||||
import com.ruoyi.iot.lock.domain.LockRoom;
|
||||
import com.ruoyi.iot.lock.domain.LockStay;
|
||||
import com.ruoyi.iot.lock.domain.LockStayCheckinBody;
|
||||
import com.ruoyi.iot.lock.domain.LockStayExternalDetail;
|
||||
import com.ruoyi.iot.lock.mapper.LockAuthMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockAccessLogMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockDeviceMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockFaceFeatureMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockPersonRefMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockRoomMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockStayMapper;
|
||||
import com.ruoyi.iot.lock.service.ILockAuthService;
|
||||
import com.ruoyi.iot.lock.service.ILockFaceService;
|
||||
import com.ruoyi.iot.lock.service.ILockStayService;
|
||||
import com.ruoyi.iot.openapi.CallbackConstants;
|
||||
import com.ruoyi.iot.openapi.config.OpenAppRegistry;
|
||||
import com.ruoyi.iot.openapi.config.OpenApiProperties;
|
||||
import com.ruoyi.iot.openapi.service.ILockCallbackService;
|
||||
import com.ruoyi.iot.person.domain.LockPerson;
|
||||
import com.ruoyi.iot.person.service.ILockPersonService;
|
||||
|
||||
/**
|
||||
* 入住单服务实现
|
||||
* <p>
|
||||
* 入住 / 退房都翻译成既有授权操作:
|
||||
* <ul>
|
||||
* <li>入住:lock_auth 新增(人脸,有效期=入住起止)并下发;</li>
|
||||
* <li>退房:锁定人员删除(facefeature delete,按人员+本房门锁),平台授权置已清除。</li>
|
||||
* </ul>
|
||||
* 人员建档与特征提取在入住事务之外执行,提取失败只回滚入住,不回滚已建的人员/人脸。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class LockStayServiceImpl implements ILockStayService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(LockStayServiceImpl.class);
|
||||
|
||||
/** 房态:空闲 */
|
||||
private static final String ROOM_STATUS_FREE = "0";
|
||||
|
||||
/** 房态:在住 */
|
||||
private static final String ROOM_STATUS_OCCUPIED = "1";
|
||||
|
||||
/** 房态:维修 */
|
||||
private static final String ROOM_STATUS_REPAIR = "2";
|
||||
|
||||
/** 房态:停用 */
|
||||
private static final String ROOM_STATUS_DISABLED = "3";
|
||||
|
||||
/** 入住状态:在住 */
|
||||
private static final String STAY_STATUS_ACTIVE = "1";
|
||||
|
||||
/** 人员类型:住户 / 员工(内部) */
|
||||
private static final String PERSON_TYPE_RESIDENT = "0";
|
||||
|
||||
private static final String PERSON_TYPE_STAFF = "1";
|
||||
|
||||
/** 访客(外部) */
|
||||
private static final String PERSON_TYPE_VISITOR = "2";
|
||||
|
||||
/** 单据来源:平台办理 */
|
||||
private static final String STAY_SOURCE_PLATFORM = "1";
|
||||
|
||||
/** 删除标志:已删除 */
|
||||
private static final String DEL_FLAG_DELETED = "2";
|
||||
|
||||
@Autowired
|
||||
private LockStayMapper stayMapper;
|
||||
|
||||
@Autowired
|
||||
private LockRoomMapper roomMapper;
|
||||
|
||||
@Autowired
|
||||
private LockDeviceMapper deviceMapper;
|
||||
|
||||
@Autowired
|
||||
private LockAuthMapper authMapper;
|
||||
|
||||
@Autowired
|
||||
private LockFaceFeatureMapper faceFeatureMapper;
|
||||
|
||||
@Autowired
|
||||
private ILockAuthService authService;
|
||||
|
||||
@Autowired
|
||||
private ILockFaceService faceService;
|
||||
|
||||
@Autowired
|
||||
private ILockPersonService personService;
|
||||
|
||||
@Autowired
|
||||
private ILockCallbackService callbackService;
|
||||
|
||||
@Autowired
|
||||
private LockPersonRefMapper personRefMapper;
|
||||
|
||||
@Autowired
|
||||
private LockAccessLogMapper accessLogMapper;
|
||||
|
||||
@Autowired
|
||||
private OpenAppRegistry openAppRegistry;
|
||||
|
||||
@Autowired
|
||||
private TransactionTemplate transactionTemplate;
|
||||
|
||||
@Override
|
||||
public List<LockStay> selectStayList(LockStay stay)
|
||||
{
|
||||
List<LockStay> list = stayMapper.selectStayList(stay);
|
||||
for (LockStay row : list)
|
||||
{
|
||||
fillBizSystemName(row);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 列表回填外部系统名称;应用已从开放管理删除时保持为空,前端回退展示编码 */
|
||||
private void fillBizSystemName(LockStay stay)
|
||||
{
|
||||
if (stay == null || StringUtils.isEmpty(stay.getBizSystem()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
OpenApiProperties.App app = openAppRegistry.getApp(stay.getBizSystem());
|
||||
if (app != null && StringUtils.isNotEmpty(app.getAppName()))
|
||||
{
|
||||
stay.setBizSystemName(app.getAppName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockStay selectStayById(Long stayId)
|
||||
{
|
||||
return stayMapper.selectStayById(stayId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockStay> selectActiveStaysByPersonNo(String personNo)
|
||||
{
|
||||
return stayMapper.selectActiveStaysByPersonNo(StringUtils.trim(personNo));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockStay> selectStaysByRoomId(Long roomId)
|
||||
{
|
||||
return stayMapper.selectStaysByRoomId(roomId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockStayExternalDetail selectExternalDetail(Long stayId)
|
||||
{
|
||||
LockStay stay = stayMapper.selectStayById(stayId);
|
||||
if (stay == null)
|
||||
{
|
||||
throw new IllegalArgumentException("入住单不存在");
|
||||
}
|
||||
LockStayExternalDetail detail = new LockStayExternalDetail();
|
||||
detail.setStayId(stay.getStayId());
|
||||
detail.setStayNo(stay.getStayNo());
|
||||
detail.setSource(stay.getSource());
|
||||
detail.setBizSystem(stay.getBizSystem());
|
||||
detail.setBizNo(stay.getBizNo());
|
||||
detail.setPersonNo(stay.getPersonNo());
|
||||
detail.setPersonName(stay.getPersonName());
|
||||
detail.setRoomCode(stay.getRoomCode());
|
||||
detail.setRoomNo(stay.getRoomNo());
|
||||
detail.setDeviceSn(stay.getDeviceSn());
|
||||
detail.setBeginTime(stay.getBeginTime());
|
||||
detail.setEndTime(stay.getEndTime());
|
||||
detail.setCheckinTime(stay.getCheckinTime());
|
||||
detail.setCheckoutTime(stay.getCheckoutTime());
|
||||
detail.setStayStatus(stay.getStayStatus());
|
||||
detail.setAuthStatus(stay.getAuthStatus());
|
||||
detail.setAuthMsg(stay.getAuthMsg());
|
||||
// 应用可能已从开放管理中删除,取不到名称时前端只展示编码
|
||||
OpenApiProperties.App app = openAppRegistry.getApp(stay.getBizSystem());
|
||||
if (app != null)
|
||||
{
|
||||
detail.setBizSystemName(app.getAppName());
|
||||
}
|
||||
LockPersonRef ref = personRefMapper.selectByAppAndPersonNo(stay.getBizSystem(), stay.getPersonNo());
|
||||
if (ref != null)
|
||||
{
|
||||
detail.setExternalPersonId(ref.getExternalId());
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockAccessLog> selectStayAccessLogs(Long stayId, String openType, String openResult)
|
||||
{
|
||||
if (stayId == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return accessLogMapper.selectAccessLogListByStay(stayId, StringUtils.trim(openType),
|
||||
StringUtils.trim(openResult));
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockStay checkin(LockStayCheckinBody body)
|
||||
{
|
||||
validateCheckin(body);
|
||||
// 预校验房间与门锁,避免为无效房间建档/提取照片
|
||||
LockRoom room = roomMapper.selectRoomById(body.getRoomId());
|
||||
if (room == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间不存在");
|
||||
}
|
||||
if (room.getDeviceId() == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间未分配门锁,请先在房间管理页分配门锁");
|
||||
}
|
||||
LockDevice device = deviceMapper.selectDeviceById(room.getDeviceId());
|
||||
if (device == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间绑定的门锁不存在");
|
||||
}
|
||||
// 1) 人员与特征(独立事务,提取是外部调用,不能放进入住事务)
|
||||
LockPerson person = preparePerson(body, device);
|
||||
// 2) 入住主流程(房间行锁 + 入住单 + 授权 + 房态)
|
||||
return transactionTemplate.execute(status -> createStay(body, person));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int checkout(Long stayId)
|
||||
{
|
||||
// 人员清理放在退房事务提交之后:清除指令已经下发,清理失败不能把退房一起回滚
|
||||
LockStay stay = transactionTemplate.execute(status -> doCheckout(stayId));
|
||||
if (stay == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
cleanupExternalPerson(stay);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private LockStay doCheckout(Long stayId)
|
||||
{
|
||||
LockStay stay = stayMapper.selectStayById(stayId);
|
||||
if (stay == null)
|
||||
{
|
||||
throw new IllegalArgumentException("入住单不存在");
|
||||
}
|
||||
if (!STAY_STATUS_ACTIVE.equals(stay.getStayStatus()))
|
||||
{
|
||||
throw new IllegalArgumentException("只有在住状态的入住单可以退房");
|
||||
}
|
||||
// 锁房间行,避免与其它入住/退房并发
|
||||
roomMapper.selectRoomByIdForUpdate(stay.getRoomId());
|
||||
Date now = new Date();
|
||||
if (stayMapper.updateCheckout(stayId, now) == 0)
|
||||
{
|
||||
throw new IllegalArgumentException("入住单状态已变更,请刷新后重试");
|
||||
}
|
||||
refreshRoomOccupancy(stay.getRoomId());
|
||||
// 删除本房门锁上该人员的人脸;失败不影响退房本身,授权状态由回执/人工跟进
|
||||
if (stay.getAuthId() != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
authService.clear(stay.getAuthId());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("退房删除人脸失败, stayId={}, authId={}, msg={}", stayId, stay.getAuthId(), e.getMessage());
|
||||
}
|
||||
LockAuth cleared = authMapper.selectAuthById(stay.getAuthId());
|
||||
if (cleared != null)
|
||||
{
|
||||
registerCallback(stay, cleared, CallbackConstants.EVENT_CHECKOUT);
|
||||
}
|
||||
}
|
||||
return stay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockStay changeRoom(Long stayId, Long roomId, Date endTime)
|
||||
{
|
||||
LockStay oldStay = stayMapper.selectStayById(stayId);
|
||||
if (oldStay == null)
|
||||
{
|
||||
throw new IllegalArgumentException("入住单不存在");
|
||||
}
|
||||
if (!STAY_STATUS_ACTIVE.equals(oldStay.getStayStatus()))
|
||||
{
|
||||
throw new IllegalArgumentException("只有在住状态的入住单可以换房");
|
||||
}
|
||||
if (roomId == null)
|
||||
{
|
||||
throw new IllegalArgumentException("请选择目标房间");
|
||||
}
|
||||
if (roomId.equals(oldStay.getRoomId()))
|
||||
{
|
||||
throw new IllegalArgumentException("目标房间与当前房间相同");
|
||||
}
|
||||
LockPerson person = requireExistingPerson(oldStay.getPersonNo());
|
||||
LockRoom target = roomMapper.selectRoomById(roomId);
|
||||
if (target == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间不存在");
|
||||
}
|
||||
// 不限制入住人数:空闲/在住都可继续办理,只有维修/停用不可
|
||||
if (ROOM_STATUS_REPAIR.equals(target.getRoomStatus())
|
||||
|| ROOM_STATUS_DISABLED.equals(target.getRoomStatus()))
|
||||
{
|
||||
throw new IllegalArgumentException("目标房间处于维修/停用状态,不可入住");
|
||||
}
|
||||
if (target.getDeviceId() == null)
|
||||
{
|
||||
throw new IllegalArgumentException("目标房间未分配门锁");
|
||||
}
|
||||
LockDevice device = deviceMapper.selectDeviceById(target.getDeviceId());
|
||||
if (device == null)
|
||||
{
|
||||
throw new IllegalArgumentException("目标房间绑定的门锁不存在");
|
||||
}
|
||||
// 换房后旧房门锁人脸会删除,因此新房必须确认能下发人脸,否则人两处都进不去
|
||||
ensureFaceReady(person, device);
|
||||
Date newEnd = endTime == null ? oldStay.getEndTime() : endTime;
|
||||
if (newEnd == null || !newEnd.after(new Date()))
|
||||
{
|
||||
throw new IllegalArgumentException("预计退房时间必须晚于当前时间");
|
||||
}
|
||||
return transactionTemplate.execute(status -> doChangeRoom(oldStay, person, target, device, newEnd));
|
||||
}
|
||||
|
||||
private LockStay doChangeRoom(LockStay oldStay, LockPerson person, LockRoom target, LockDevice device,
|
||||
Date newEnd)
|
||||
{
|
||||
LockRoom locked = roomMapper.selectRoomByIdForUpdate(target.getRoomId());
|
||||
if (locked == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间不存在");
|
||||
}
|
||||
// 不限制入住人数:空闲/在住都可作为目标房间,只有维修/停用不可
|
||||
if (ROOM_STATUS_REPAIR.equals(locked.getRoomStatus())
|
||||
|| ROOM_STATUS_DISABLED.equals(locked.getRoomStatus()))
|
||||
{
|
||||
throw new IllegalArgumentException("目标房间处于维修/停用状态,不可入住");
|
||||
}
|
||||
Date now = new Date();
|
||||
String stayNo = nextStayNo();
|
||||
LockAuth auth = new LockAuth();
|
||||
auth.setPersonNo(person.getPersonNo());
|
||||
auth.setPersonName(person.getPersonName());
|
||||
auth.setDeviceId(device.getDeviceId());
|
||||
auth.setAuthType(LockConstants.AUTH_FACE);
|
||||
auth.setBeginTime(now);
|
||||
auth.setEndTime(newEnd);
|
||||
auth.setRemark("换房 " + stayNo);
|
||||
authService.insertAuth(auth);
|
||||
|
||||
LockStay newStay = new LockStay();
|
||||
newStay.setStayNo(stayNo);
|
||||
newStay.setPersonId(person.getPersonId());
|
||||
newStay.setPersonNo(person.getPersonNo());
|
||||
newStay.setPersonName(person.getPersonName());
|
||||
newStay.setGuestType(oldStay.getGuestType());
|
||||
newStay.setRoomId(locked.getRoomId());
|
||||
newStay.setRoomCode(locked.getRoomCode());
|
||||
newStay.setDeviceId(device.getDeviceId());
|
||||
newStay.setDeviceSn(device.getDeviceSn());
|
||||
newStay.setAuthId(auth.getAuthId());
|
||||
newStay.setBeginTime(now);
|
||||
newStay.setEndTime(newEnd);
|
||||
newStay.setCheckinTime(now);
|
||||
newStay.setChangeFromId(oldStay.getStayId());
|
||||
newStay.setStayStatus(STAY_STATUS_ACTIVE);
|
||||
newStay.setAuthStatus(LockConstants.SYNC_PENDING);
|
||||
newStay.setRemark(oldStay.getRemark());
|
||||
// 新单仍归属原业务系统,但不继承 biz_no:(biz_system, biz_no) 是唯一键,
|
||||
// 沿用会与原单撞键;对方仍可按「人员 + 房间」定位换房后的在住单
|
||||
newStay.setBizSystem(oldStay.getBizSystem());
|
||||
newStay.setSource(StringUtils.isEmpty(oldStay.getSource()) ? STAY_SOURCE_PLATFORM : oldStay.getSource());
|
||||
stayMapper.insertStay(newStay);
|
||||
|
||||
LockAuth savedAuth = authMapper.selectAuthById(auth.getAuthId());
|
||||
if (savedAuth != null)
|
||||
{
|
||||
newStay.setAuthStatus(savedAuth.getSyncStatus());
|
||||
newStay.setAuthMsg(savedAuth.getSyncMsg());
|
||||
stayMapper.updateAuthStatusByAuthId(auth.getAuthId(), savedAuth.getSyncStatus(), savedAuth.getSyncMsg());
|
||||
}
|
||||
refreshRoomOccupancy(locked.getRoomId());
|
||||
|
||||
// 结束旧单并释放旧房;旧单条件更新失败说明并发已处理,整体回滚
|
||||
if (stayMapper.updateChanged(oldStay.getStayId(), now) == 0)
|
||||
{
|
||||
throw new IllegalArgumentException("原入住单状态已变更,请刷新后重试");
|
||||
}
|
||||
refreshRoomOccupancy(oldStay.getRoomId());
|
||||
// 删除旧房门锁上该人员的人脸;失败不影响新单,旧人脸保留到其有效期结束
|
||||
if (oldStay.getAuthId() != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
authService.clear(oldStay.getAuthId());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("换房删除旧房人脸失败, stayId={}, authId={}, msg={}", oldStay.getStayId(),
|
||||
oldStay.getAuthId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
log.info("换房完成, oldStayId={}, newStayNo={}, personNo={}, fromRoom={}, toRoom={}", oldStay.getStayId(),
|
||||
stayNo, person.getPersonNo(), oldStay.getRoomCode(), locked.getRoomCode());
|
||||
return newStay;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int extend(Long stayId, Date endTime)
|
||||
{
|
||||
LockStay stay = stayMapper.selectStayById(stayId);
|
||||
if (stay == null)
|
||||
{
|
||||
throw new IllegalArgumentException("入住单不存在");
|
||||
}
|
||||
if (!STAY_STATUS_ACTIVE.equals(stay.getStayStatus()))
|
||||
{
|
||||
throw new IllegalArgumentException("只有在住状态的入住单可以续住");
|
||||
}
|
||||
if (endTime == null)
|
||||
{
|
||||
throw new IllegalArgumentException("请选择新的预计退房时间");
|
||||
}
|
||||
if (!endTime.after(new Date()))
|
||||
{
|
||||
throw new IllegalArgumentException("预计退房时间必须晚于当前时间");
|
||||
}
|
||||
if (stay.getEndTime() != null && !endTime.after(stay.getEndTime()))
|
||||
{
|
||||
throw new IllegalArgumentException("新的预计退房时间必须晚于当前预计退房时间");
|
||||
}
|
||||
int rows = stayMapper.updateEndTime(stayId, endTime);
|
||||
if (rows == 0)
|
||||
{
|
||||
throw new IllegalArgumentException("入住单状态已变更,请刷新后重试");
|
||||
}
|
||||
if (stay.getAuthId() != null)
|
||||
{
|
||||
LockAuth auth = authMapper.selectAuthById(stay.getAuthId());
|
||||
if (auth != null)
|
||||
{
|
||||
// 重新下发 add 帧,更新设备侧授权有效期
|
||||
auth.setEndTime(endTime);
|
||||
authService.updateAuth(auth);
|
||||
LockAuth latest = authMapper.selectAuthById(stay.getAuthId());
|
||||
if (latest != null)
|
||||
{
|
||||
registerCallback(stay, latest, CallbackConstants.EVENT_EXTEND);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("续住完成, stayNo={}, personNo={}, newEndTime={}", stay.getStayNo(), stay.getPersonNo(), endTime);
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expireStays(int limit)
|
||||
{
|
||||
int batch = limit <= 0 ? 200 : limit;
|
||||
List<LockStay> expired = stayMapper.selectExpiredStays(new Date(), batch);
|
||||
int count = 0;
|
||||
for (LockStay stay : expired)
|
||||
{
|
||||
try
|
||||
{
|
||||
Boolean done = transactionTemplate.execute(status -> expireOne(stay));
|
||||
if (Boolean.TRUE.equals(done))
|
||||
{
|
||||
count++;
|
||||
cleanupExternalPerson(stay);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// 单条失败不影响其它入住单
|
||||
log.warn("入住到期处理失败, stayId={}, msg={}", stay.getStayId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
if (count > 0)
|
||||
{
|
||||
log.info("入住到期处理完成, count={}, batch={}", count, expired.size());
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** 单条到期处理:条件更新防止与退房/换房并发冲突 */
|
||||
private boolean expireOne(LockStay stay)
|
||||
{
|
||||
if (stayMapper.updateExpire(stay.getStayId()) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
refreshRoomOccupancy(stay.getRoomId());
|
||||
if (stay.getAuthId() != null)
|
||||
{
|
||||
// 设备侧授权已按有效期自动失效;本地置「已清除」以停止补偿,避免到期后又把人脸推回设备
|
||||
authMapper.updateSyncStatus(stay.getAuthId(), LockConstants.SYNC_CLEARED, "入住到期,平台置为已清除");
|
||||
}
|
||||
log.info("入住到期自动退房, stayNo={}, personNo={}, roomCode={}", stay.getStayNo(), stay.getPersonNo(),
|
||||
stay.getRoomCode());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退房(含到期自动退房)清理外部接口自动建档的人员:本地照片文件、人脸、各版本特征、人员档案
|
||||
* 一并删除,外部标识映射保留,下次入住时重新建档并改指。
|
||||
* <p>
|
||||
* 只处理开放接口建档(create_by=openapi:*)、未绑定平台账号,且名下已无其它未结束入住单
|
||||
* 与未清除授权的人员;任一守卫不满足都保留档案,避免误删仍要下发或仍在设备上生效的授权。
|
||||
* 映射不删的原因:退房回调可能在设备回执后才生成报文,仍需按旧 person_no 解析 externalPersonId。
|
||||
* <p>
|
||||
* 在退房事务提交后独立执行,失败只记日志,不影响已完成的退房。
|
||||
*/
|
||||
private void cleanupExternalPerson(LockStay stay)
|
||||
{
|
||||
if (stay == null || StringUtils.isEmpty(stay.getBizSystem()) || stay.getPersonId() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
transactionTemplate.executeWithoutResult(status -> doCleanupExternalPerson(stay));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("退房清理外部人员失败, stayNo={}, personNo={}, msg={}", stay.getStayNo(), stay.getPersonNo(),
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void doCleanupExternalPerson(LockStay stay)
|
||||
{
|
||||
LockPerson person = personService.selectPersonById(stay.getPersonId());
|
||||
if (person == null || DEL_FLAG_DELETED.equals(person.getDelFlag())
|
||||
|| !StringUtils.startsWith(StringUtils.trim(person.getCreateBy()), LockConstants.CREATE_BY_OPENAPI)
|
||||
|| person.getSysUserId() != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (stayMapper.countActiveStaysByPersonNo(person.getPersonNo(), stay.getStayId()) > 0)
|
||||
{
|
||||
log.debug("人员仍有其它入住单,保留档案, personNo={}", person.getPersonNo());
|
||||
return;
|
||||
}
|
||||
// 本单授权刚发起清除、回执可能未到,不计入;其余未清除授权说明人员仍在别的门锁上有效
|
||||
if (authMapper.countActiveAuthsByPersonNo(person.getPersonNo(), stay.getAuthId()) > 0)
|
||||
{
|
||||
log.info("人员仍有未清除授权,保留档案, personNo={}, stayNo={}", person.getPersonNo(), stay.getStayNo());
|
||||
return;
|
||||
}
|
||||
LockFace face = faceService.selectFaceByPersonNo(person.getPersonNo());
|
||||
if (face != null)
|
||||
{
|
||||
// 先删照片文件再删记录:记录删掉后就再也定位不到这张照片;特征是派生数据随人脸一起删
|
||||
faceService.deleteLocalPhotoFile(face.getPhotoUrl());
|
||||
faceService.deleteFaceByIds(new Long[] { face.getFaceId() });
|
||||
}
|
||||
personService.deletePersonRecord(person.getPersonId());
|
||||
log.info("退房清理外部人员完成, stayNo={}, personNo={}, faceId={}", stay.getStayNo(), person.getPersonNo(),
|
||||
face == null ? null : face.getFaceId());
|
||||
}
|
||||
|
||||
/** 换房前确认目标门锁能取到该人员对应算法版本的人脸特征(门锁未配版本时由下发逻辑回退) */
|
||||
private void ensureFaceReady(LockPerson person, LockDevice device)
|
||||
{
|
||||
String version = StringUtils.trim(device.getFaceAlgoVersion());
|
||||
if (StringUtils.isEmpty(version))
|
||||
{
|
||||
return;
|
||||
}
|
||||
LockFaceFeature feature = faceFeatureMapper.selectUsableFeature(person.getPersonNo(), version);
|
||||
if (feature == null || StringUtils.isEmpty(feature.getFeatureData()))
|
||||
{
|
||||
throw new IllegalArgumentException("该人员缺少门锁算法版本(" + version
|
||||
+ ")的人脸特征,请先在人脸数据页按该版本提取后再换房");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析人员:newPerson 非空时建档 + 建人脸 + 按目标门锁版本提取特征
|
||||
*/
|
||||
private LockPerson preparePerson(LockStayCheckinBody body, LockDevice device)
|
||||
{
|
||||
if (body.getNewPerson() == null)
|
||||
{
|
||||
return requireExistingPerson(body.getPersonNo());
|
||||
}
|
||||
LockStayCheckinBody.NewPerson np = body.getNewPerson();
|
||||
if (StringUtils.isEmpty(StringUtils.trim(np.getPhotoUrl())))
|
||||
{
|
||||
throw new IllegalArgumentException("外部人员必须上传人脸照片");
|
||||
}
|
||||
if (StringUtils.isEmpty(StringUtils.trim(np.getPersonName())))
|
||||
{
|
||||
throw new IllegalArgumentException("外部人员姓名不能为空");
|
||||
}
|
||||
LockPerson created = new LockPerson();
|
||||
created.setPersonNo(StringUtils.trim(np.getPersonNo()));
|
||||
created.setPersonName(StringUtils.trim(np.getPersonName()));
|
||||
created.setPersonType(StringUtils.isEmpty(np.getPersonType()) ? PERSON_TYPE_VISITOR
|
||||
: StringUtils.trim(np.getPersonType()));
|
||||
created.setIdCardNo(StringUtils.trim(np.getIdCardNo()));
|
||||
created.setPhone(StringUtils.trim(np.getPhone()));
|
||||
created.setStatus(LockConstants.PERSON_STATUS_NORMAL);
|
||||
// personNo 留空时由人员服务自动生成
|
||||
personService.insertPerson(created);
|
||||
|
||||
LockFace face = new LockFace();
|
||||
face.setPersonNo(created.getPersonNo());
|
||||
face.setPersonName(created.getPersonName());
|
||||
face.setPhotoUrl(StringUtils.trim(np.getPhotoUrl()));
|
||||
// 人脸记录状态:0 正常
|
||||
face.setStatus("0");
|
||||
faceService.insertFace(face);
|
||||
|
||||
String feature = faceService.extract(face.getFaceId(), device.getDeviceId());
|
||||
if (StringUtils.isEmpty(feature))
|
||||
{
|
||||
// 人员与人脸记录保留,便于重传照片后再次办理入住
|
||||
throw new IllegalArgumentException("人脸特征提取失败,请检查照片质量或门锁算法版本配置后重试");
|
||||
}
|
||||
// 回填 body 的 personNo,后续建档走同一人员
|
||||
body.setPersonNo(created.getPersonNo());
|
||||
return created;
|
||||
}
|
||||
|
||||
private LockPerson requireExistingPerson(String personNo)
|
||||
{
|
||||
String trimmed = StringUtils.trim(personNo);
|
||||
if (StringUtils.isEmpty(trimmed))
|
||||
{
|
||||
throw new IllegalArgumentException("请选择人员或填写新建人员信息");
|
||||
}
|
||||
LockPerson person = personService.selectPersonByNo(trimmed);
|
||||
if (person == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人员主数据不存在:" + trimmed);
|
||||
}
|
||||
if (!LockConstants.PERSON_STATUS_NORMAL.equals(person.getStatus()))
|
||||
{
|
||||
throw new IllegalArgumentException("人员已停用,无法办理入住:" + trimmed);
|
||||
}
|
||||
Date now = new Date();
|
||||
if (person.getValidFrom() != null && person.getValidFrom().after(now))
|
||||
{
|
||||
throw new IllegalArgumentException("人员尚未到生效时间,无法办理入住:" + trimmed);
|
||||
}
|
||||
if (person.getValidTo() != null && person.getValidTo().before(now))
|
||||
{
|
||||
throw new IllegalArgumentException("人员已过有效期,无法办理入住:" + trimmed);
|
||||
}
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* 入住主流程(事务内):复核房态 → 建授权 → 建入住单 → 更新房态
|
||||
*/
|
||||
private LockStay createStay(LockStayCheckinBody body, LockPerson person)
|
||||
{
|
||||
LockRoom room = roomMapper.selectRoomByIdForUpdate(body.getRoomId());
|
||||
if (room == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间不存在");
|
||||
}
|
||||
// 不限制房间入住人数:空闲/在住都可继续办理,只有维修/停用不可入住
|
||||
if (ROOM_STATUS_REPAIR.equals(room.getRoomStatus()) || ROOM_STATUS_DISABLED.equals(room.getRoomStatus()))
|
||||
{
|
||||
throw new IllegalArgumentException("房间处于维修/停用状态,不可办理入住");
|
||||
}
|
||||
if (room.getDeviceId() == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间未分配门锁,请先分配门锁");
|
||||
}
|
||||
LockDevice device = deviceMapper.selectDeviceById(room.getDeviceId());
|
||||
if (device == null)
|
||||
{
|
||||
throw new IllegalArgumentException("房间绑定的门锁不存在");
|
||||
}
|
||||
|
||||
String stayNo = nextStayNo();
|
||||
LockAuth auth = new LockAuth();
|
||||
auth.setPersonNo(person.getPersonNo());
|
||||
auth.setPersonName(person.getPersonName());
|
||||
auth.setDeviceId(device.getDeviceId());
|
||||
auth.setAuthType(LockConstants.AUTH_FACE);
|
||||
// 补录历史入住时不能下发一个已失效的授权,生效起取 max(now, begin)
|
||||
Date begin = body.getBeginTime().before(new Date()) ? new Date() : body.getBeginTime();
|
||||
auth.setBeginTime(begin);
|
||||
auth.setEndTime(body.getEndTime());
|
||||
auth.setRemark("入住 " + stayNo);
|
||||
authService.insertAuth(auth);
|
||||
|
||||
LockStay stay = new LockStay();
|
||||
stay.setStayNo(stayNo);
|
||||
stay.setPersonId(person.getPersonId());
|
||||
stay.setPersonNo(person.getPersonNo());
|
||||
stay.setPersonName(person.getPersonName());
|
||||
stay.setGuestType(resolveGuestType(body.getGuestType(), person));
|
||||
stay.setRoomId(room.getRoomId());
|
||||
stay.setRoomCode(room.getRoomCode());
|
||||
stay.setDeviceId(device.getDeviceId());
|
||||
stay.setDeviceSn(device.getDeviceSn());
|
||||
stay.setAuthId(auth.getAuthId());
|
||||
stay.setBeginTime(body.getBeginTime());
|
||||
stay.setEndTime(body.getEndTime());
|
||||
stay.setCheckinTime(new Date());
|
||||
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()));
|
||||
stayMapper.insertStay(stay);
|
||||
|
||||
// 授权可能因门锁无路由/缺特征在创建时即失败,回读真实状态写入入住单
|
||||
LockAuth savedAuth = authMapper.selectAuthById(auth.getAuthId());
|
||||
if (savedAuth != null)
|
||||
{
|
||||
stay.setAuthStatus(savedAuth.getSyncStatus());
|
||||
stay.setAuthMsg(savedAuth.getSyncMsg());
|
||||
stayMapper.updateAuthStatusByAuthId(auth.getAuthId(), savedAuth.getSyncStatus(), savedAuth.getSyncMsg());
|
||||
}
|
||||
|
||||
refreshRoomOccupancy(room.getRoomId());
|
||||
registerCallback(stay, savedAuth == null ? auth : savedAuth, CallbackConstants.EVENT_CHECKIN);
|
||||
log.info("入住登记完成, stayNo={}, personNo={}, roomCode={}, deviceSn={}, authId={}", stayNo,
|
||||
person.getPersonNo(), room.getRoomCode(), device.getDeviceSn(), auth.getAuthId());
|
||||
return stay;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登记对外回调事件。回调是旁路能力:登记失败只记日志,不能让入住/退房/续住回滚,
|
||||
* 任务异常可通过回调任务列表人工排查与重发。
|
||||
*/
|
||||
private void registerCallback(LockStay stay, LockAuth auth, String eventType)
|
||||
{
|
||||
try
|
||||
{
|
||||
callbackService.registerStayEvent(stay.getStayId(), auth, eventType);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("回调事件登记失败, stayNo={}, eventType={}, msg={}", stay.getStayNo(), eventType, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重算房间占用:一房可住多人,仍有在住单就保持「在住」并记录最近一单,
|
||||
* 全部退房后才置「空闲」并清空冗余字段。
|
||||
*/
|
||||
private void refreshRoomOccupancy(Long roomId)
|
||||
{
|
||||
if (roomId == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<LockStay> active = stayMapper.selectActiveStaysByRoomId(roomId);
|
||||
if (active.isEmpty())
|
||||
{
|
||||
roomMapper.updateRoomOccupancy(roomId, ROOM_STATUS_FREE, null, null);
|
||||
return;
|
||||
}
|
||||
LockStay latest = active.get(0);
|
||||
roomMapper.updateRoomOccupancy(roomId, ROOM_STATUS_OCCUPIED, latest.getStayId(), latest.getPersonNo());
|
||||
}
|
||||
|
||||
private void validateCheckin(LockStayCheckinBody body)
|
||||
{
|
||||
if (body == null || body.getRoomId() == null)
|
||||
{
|
||||
throw new IllegalArgumentException("请选择房间");
|
||||
}
|
||||
if (body.getBeginTime() == null || body.getEndTime() == null)
|
||||
{
|
||||
throw new IllegalArgumentException("请填写入住开始与结束时间");
|
||||
}
|
||||
if (!body.getEndTime().after(body.getBeginTime()))
|
||||
{
|
||||
throw new IllegalArgumentException("预计退房时间必须晚于入住开始时间");
|
||||
}
|
||||
}
|
||||
|
||||
/** 入住类型:优先取下拉值,未填时按人员类型映射(0住户/1员工 → 内部,其余 → 外部) */
|
||||
private String resolveGuestType(String guestType, LockPerson person)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(StringUtils.trim(guestType)))
|
||||
{
|
||||
return StringUtils.trim(guestType);
|
||||
}
|
||||
String personType = person.getPersonType();
|
||||
return PERSON_TYPE_RESIDENT.equals(personType) || PERSON_TYPE_STAFF.equals(personType) ? "0" : "1";
|
||||
}
|
||||
|
||||
private String nextStayNo()
|
||||
{
|
||||
return "STAY" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
|
||||
+ StringUtils.substring(IdUtils.fastSimpleUUID(), 0, 4).toUpperCase();
|
||||
}
|
||||
}
|
||||
+770
@@ -0,0 +1,770 @@
|
||||
package com.ruoyi.iot.openapi.service.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.iot.lock.LockConstants;
|
||||
import com.ruoyi.iot.lock.domain.LockDevice;
|
||||
import com.ruoyi.iot.lock.domain.LockFace;
|
||||
import com.ruoyi.iot.lock.domain.LockFaceFeature;
|
||||
import com.ruoyi.iot.lock.domain.LockPersonRef;
|
||||
import com.ruoyi.iot.lock.domain.LockRoom;
|
||||
import com.ruoyi.iot.lock.domain.LockStay;
|
||||
import com.ruoyi.iot.lock.domain.LockStayCheckinBody;
|
||||
import com.ruoyi.iot.lock.mapper.LockDeviceMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockPersonRefMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockRoomMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockStayMapper;
|
||||
import com.ruoyi.iot.lock.service.ILockFaceService;
|
||||
import com.ruoyi.iot.lock.service.ILockStayService;
|
||||
import com.ruoyi.iot.openapi.OpenApiCodes;
|
||||
import com.ruoyi.iot.openapi.OpenApiException;
|
||||
import com.ruoyi.iot.openapi.config.OpenAppRegistry;
|
||||
import com.ruoyi.iot.openapi.config.OpenApiProperties.App;
|
||||
import com.ruoyi.iot.openapi.domain.OpenStayCheckinRequest;
|
||||
import com.ruoyi.iot.openapi.domain.OpenStayCheckoutRequest;
|
||||
import com.ruoyi.iot.openapi.domain.OpenStayExtendRequest;
|
||||
import com.ruoyi.iot.openapi.domain.OpenStayResult;
|
||||
import com.ruoyi.iot.openapi.service.IOpenStayService;
|
||||
import com.ruoyi.iot.person.domain.LockPerson;
|
||||
import com.ruoyi.iot.person.service.ILockPersonService;
|
||||
|
||||
/**
|
||||
* 开放接口-住宿业务服务实现
|
||||
* <p>
|
||||
* 复用住宿模块:入住/退房/续住最终都落到 {@link ILockStayService},本类只负责
|
||||
* 对外契约相关的三件事——按 {@code appCode} 隔离与幂等、外部人员标识映射、
|
||||
* 头像解析与按门锁算法版本提取特征。
|
||||
* <p>
|
||||
* 人员建档与特征提取在入住事务之外(提取是外部 HTTP 调用),失败时人员与人脸
|
||||
* 记录保留,便于对方修正照片后用同一 {@code bizNo} 之外的新单重试。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class OpenStayServiceImpl implements IOpenStayService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenStayServiceImpl.class);
|
||||
|
||||
/** 对外时间格式 */
|
||||
private static final String TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/** 单据来源:接口办理 */
|
||||
private static final String STAY_SOURCE_API = "2";
|
||||
|
||||
/** 入住状态:在住 */
|
||||
private static final String STAY_STATUS_ACTIVE = "1";
|
||||
|
||||
/** 房态:维修 / 停用(不可办理入住) */
|
||||
private static final String ROOM_STATUS_REPAIR = "2";
|
||||
|
||||
private static final String ROOM_STATUS_DISABLED = "3";
|
||||
|
||||
/** 人员类型默认值:访客 */
|
||||
private static final String PERSON_TYPE_VISITOR = "2";
|
||||
|
||||
/** 人脸记录状态:正常 */
|
||||
private static final String FACE_STATUS_NORMAL = "0";
|
||||
|
||||
/** base64 头像解码后上限(2MB) */
|
||||
private static final int PHOTO_MAX_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
/** 删除标志:已删除 */
|
||||
private static final String DEL_FLAG_DELETED = "2";
|
||||
|
||||
@Autowired
|
||||
private LockStayMapper stayMapper;
|
||||
|
||||
@Autowired
|
||||
private LockRoomMapper roomMapper;
|
||||
|
||||
@Autowired
|
||||
private LockDeviceMapper deviceMapper;
|
||||
|
||||
@Autowired
|
||||
private LockPersonRefMapper personRefMapper;
|
||||
|
||||
@Autowired
|
||||
private ILockStayService stayService;
|
||||
|
||||
@Autowired
|
||||
private ILockPersonService personService;
|
||||
|
||||
@Autowired
|
||||
private ILockFaceService faceService;
|
||||
|
||||
@Autowired
|
||||
private OpenAppRegistry openAppRegistry;
|
||||
|
||||
@Override
|
||||
public OpenStayResult checkin(String appCode, OpenStayCheckinRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "请求体不能为空");
|
||||
}
|
||||
String bizNo = requireText(request.getBizNo(), "bizNo");
|
||||
String externalPersonId = requireText(request.getExternalPersonId(), "externalPersonId");
|
||||
String personName = requireText(request.getPersonName(), "personName");
|
||||
String roomNo = requireText(request.getRoomNo(), "roomNo");
|
||||
Date beginTime = parseTime(request.getBeginTime(), "beginTime");
|
||||
Date endTime = parseTime(request.getEndTime(), "endTime");
|
||||
if (!endTime.after(beginTime))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.TIME_INVALID, "endTime 必须晚于 beginTime");
|
||||
}
|
||||
|
||||
// 幂等:同一应用下 bizNo 只建一单,重复提交返回首次结果与当前状态
|
||||
LockStay existing = stayMapper.selectStayByBiz(appCode, bizNo);
|
||||
if (existing != null)
|
||||
{
|
||||
return repeatCheckin(existing, appCode, bizNo, externalPersonId, roomNo);
|
||||
}
|
||||
// 每次入住都必须带照片:平台不再沿用历史照片,缺参直接在建档前拒绝,避免留下孤立人员
|
||||
if (StringUtils.isEmpty(StringUtils.trim(request.getPhotoUrl()))
|
||||
&& StringUtils.isEmpty(StringUtils.trim(request.getPhotoBase64())))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "photoUrl 与 photoBase64 必须提供一个");
|
||||
}
|
||||
|
||||
LockRoom room = requireUniqueRoom(roomNo);
|
||||
requireCheckinable(room);
|
||||
LockDevice device = requireRoomDevice(room);
|
||||
|
||||
String personNo = resolvePerson(appCode, externalPersonId, personName, request, device);
|
||||
// 同房多人允许办理,但要让对方知道该人员在本房间已有在住单
|
||||
boolean duplicate = hasActiveStayInRoom(personNo, room.getRoomId());
|
||||
|
||||
LockStayCheckinBody body = new LockStayCheckinBody();
|
||||
body.setRoomId(room.getRoomId());
|
||||
body.setPersonNo(personNo);
|
||||
body.setGuestType(request.getGuestType());
|
||||
body.setBeginTime(beginTime);
|
||||
body.setEndTime(endTime);
|
||||
body.setRemark(request.getRemark());
|
||||
body.setBizNo(bizNo);
|
||||
body.setBizSystem(appCode);
|
||||
body.setSource(STAY_SOURCE_API);
|
||||
|
||||
LockStay stay;
|
||||
try
|
||||
{
|
||||
stay = stayService.checkin(body);
|
||||
}
|
||||
catch (DuplicateKeyException e)
|
||||
{
|
||||
// 并发下同一 bizNo 已被另一请求建单:回查后按幂等返回,不重复建房
|
||||
LockStay winner = stayMapper.selectStayByBiz(appCode, bizNo);
|
||||
if (winner == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.INTERNAL_ERROR, "入住单写入冲突,请重试");
|
||||
}
|
||||
log.warn("入住并发撞业务号, appCode={}, bizNo={}, stayNo={}", appCode, bizNo, winner.getStayNo());
|
||||
return toResult(winner, appCode);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw translateStayError(e.getMessage());
|
||||
}
|
||||
// 入住单落库后回读,拿到联表的房号与回写后的授权状态
|
||||
LockStay saved = stayMapper.selectStayById(stay.getStayId());
|
||||
OpenStayResult result = toResult(saved == null ? stay : saved, appCode);
|
||||
if (duplicate)
|
||||
{
|
||||
result.setDuplicatePersonInRoom(Boolean.TRUE);
|
||||
}
|
||||
log.info("开放接口入住受理, appCode={}, bizNo={}, stayNo={}, personNo={}, roomNo={}, authStatus={}", appCode,
|
||||
bizNo, result.getStayNo(), personNo, roomNo, result.getAuthStatus());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenStayResult checkout(String appCode, OpenStayCheckoutRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "请求体不能为空");
|
||||
}
|
||||
LockStay stay = locateStay(appCode, request.getBizNo(), request.getPersonNo(), request.getExternalPersonId(),
|
||||
request.getRoomNo());
|
||||
if (!STAY_STATUS_ACTIVE.equals(stay.getStayStatus()))
|
||||
{
|
||||
// 已退房/已换房:幂等返回当前状态,不重复下发删除
|
||||
log.info("开放接口退房幂等返回, appCode={}, stayNo={}, stayStatus={}", appCode, stay.getStayNo(),
|
||||
stay.getStayStatus());
|
||||
return toResult(stay, appCode);
|
||||
}
|
||||
try
|
||||
{
|
||||
stayService.checkout(stay.getStayId());
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.STAY_NOT_FOUND, e.getMessage());
|
||||
}
|
||||
LockStay latest = stayMapper.selectStayById(stay.getStayId());
|
||||
log.info("开放接口退房受理, appCode={}, stayNo={}, personNo={}, roomNo={}", appCode, stay.getStayNo(),
|
||||
stay.getPersonNo(), stay.getRoomNo());
|
||||
return toResult(latest == null ? stay : latest, appCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenStayResult extend(String appCode, OpenStayExtendRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "请求体不能为空");
|
||||
}
|
||||
Date endTime = parseTime(request.getEndTime(), "endTime");
|
||||
LockStay stay = locateStay(appCode, request.getBizNo(), request.getPersonNo(), request.getExternalPersonId(),
|
||||
request.getRoomNo());
|
||||
if (!STAY_STATUS_ACTIVE.equals(stay.getStayStatus()))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.STAY_NOT_FOUND,
|
||||
"入住单不是在住状态,不能续住,stayNo=" + stay.getStayNo());
|
||||
}
|
||||
if (!endTime.after(new Date()))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.TIME_INVALID, "endTime 必须晚于当前时间");
|
||||
}
|
||||
Date previous = stay.getEndTime();
|
||||
if (previous != null && endTime.compareTo(previous) == 0)
|
||||
{
|
||||
// 与当前结束时间一致:幂等成功,不重复下发授权
|
||||
OpenStayResult result = toResult(stay, appCode);
|
||||
result.setPreviousEndTime(previous);
|
||||
return result;
|
||||
}
|
||||
if (previous != null && endTime.before(previous))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.TIME_INVALID,
|
||||
"endTime 必须晚于原结束时间 " + formatTime(previous));
|
||||
}
|
||||
try
|
||||
{
|
||||
stayService.extend(stay.getStayId(), endTime);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw translateStayError(e.getMessage());
|
||||
}
|
||||
LockStay latest = stayMapper.selectStayById(stay.getStayId());
|
||||
OpenStayResult result = toResult(latest == null ? stay : latest, appCode);
|
||||
result.setPreviousEndTime(previous);
|
||||
log.info("开放接口续住受理, appCode={}, stayNo={}, previousEndTime={}, endTime={}", appCode, stay.getStayNo(),
|
||||
formatTime(previous), formatTime(endTime));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位入住单:优先 {@code bizNo},否则「人员标识 + 房号」。
|
||||
* 两条路径都带 {@code appCode} 过滤,跨应用的单号/人员一律视为不存在。
|
||||
*/
|
||||
private LockStay locateStay(String appCode, String bizNo, String personNo, String externalPersonId, String roomNo)
|
||||
{
|
||||
String biz = StringUtils.trim(bizNo);
|
||||
if (StringUtils.isNotEmpty(biz))
|
||||
{
|
||||
LockStay stay = stayMapper.selectStayByBiz(appCode, biz);
|
||||
if (stay == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.STAY_NOT_FOUND,
|
||||
"入住单不存在或不属于当前应用,bizNo=" + biz);
|
||||
}
|
||||
return stay;
|
||||
}
|
||||
String resolvedPersonNo = resolvePersonNo(appCode, personNo, externalPersonId);
|
||||
String no = StringUtils.trim(roomNo);
|
||||
if (StringUtils.isEmpty(no))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "bizNo 或「personNo/externalPersonId + roomNo」必须提供其一");
|
||||
}
|
||||
LockRoom room = requireUniqueRoom(no);
|
||||
LockStay active = stayMapper.selectActiveStayByPersonAndRoom(resolvedPersonNo, room.getRoomId(), appCode);
|
||||
if (active != null)
|
||||
{
|
||||
return active;
|
||||
}
|
||||
// 无在住单:已退房时按幂等返回,从未入住才是「不存在」
|
||||
LockStay latest = stayMapper.selectLatestStayByPersonAndRoom(resolvedPersonNo, room.getRoomId(), appCode);
|
||||
if (latest == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.STAY_NOT_FOUND,
|
||||
"未找到该人员在本应用下该房间的入住单,roomNo=" + no);
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
/** 重复提交同一 bizNo:人员与房间一致才按幂等返回,否则说明对方串号 */
|
||||
private OpenStayResult repeatCheckin(LockStay existing, String appCode, String bizNo, String externalPersonId,
|
||||
String roomNo)
|
||||
{
|
||||
if (DEL_FLAG_DELETED.equals(existing.getDelFlag()))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.BIZ_NO_CONFLICT,
|
||||
"bizNo 已被一条已删除的入住单占用,请更换 bizNo:" + bizNo);
|
||||
}
|
||||
LockPersonRef ref = personRefMapper.selectByAppAndExternalId(appCode, externalPersonId);
|
||||
boolean samePerson = ref != null && ref.getPersonNo().equals(existing.getPersonNo());
|
||||
boolean sameRoom = roomNo.equals(existing.getRoomNo());
|
||||
if (!samePerson || !sameRoom)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.BIZ_NO_CONFLICT,
|
||||
"bizNo 已存在且人员或房间与首次请求不一致,bizNo=" + bizNo);
|
||||
}
|
||||
log.info("开放接口入住幂等返回, appCode={}, bizNo={}, stayNo={}, stayStatus={}", appCode, bizNo,
|
||||
existing.getStayNo(), existing.getStayStatus());
|
||||
return toResult(existing, appCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析人员:外部标识已映射则复用平台人员,否则建档并写映射。
|
||||
* <p>
|
||||
* 映射存在但人员已被退房清理时,按本次入住重新建档并改指映射——外部系统无需感知
|
||||
* 平台内部的档案生命周期。无论新旧人员,都确保其在目标门锁算法版本下有可用特征。
|
||||
*/
|
||||
private String resolvePerson(String appCode, String externalPersonId, String personName,
|
||||
OpenStayCheckinRequest request, LockDevice device)
|
||||
{
|
||||
LockPersonRef ref = personRefMapper.selectByAppAndExternalId(appCode, externalPersonId);
|
||||
LockPerson person;
|
||||
if (ref == null)
|
||||
{
|
||||
person = createOpenApiPerson(appCode, externalPersonId, personName, request);
|
||||
String mapped = savePersonRef(appCode, externalPersonId, person);
|
||||
if (!mapped.equals(person.getPersonNo()))
|
||||
{
|
||||
// 并发首次入住:另一请求先写了映射,改用它的人员,本次新建的档案留待人工合并
|
||||
person = requireMappedPerson(mapped, externalPersonId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
person = personService.selectPersonByNo(ref.getPersonNo());
|
||||
if (person == null)
|
||||
{
|
||||
// 上次退房时已清理该人员:重新建档并把映射改指过去
|
||||
person = createOpenApiPerson(appCode, externalPersonId, personName, request);
|
||||
String mapped = rebindPersonRef(appCode, externalPersonId, ref, person);
|
||||
if (!mapped.equals(person.getPersonNo()))
|
||||
{
|
||||
person = requireMappedPerson(mapped, externalPersonId);
|
||||
}
|
||||
}
|
||||
}
|
||||
validatePersonUsable(person);
|
||||
ensureFaceFeature(person, request, device);
|
||||
return person.getPersonNo();
|
||||
}
|
||||
|
||||
/** 按开放接口请求建档,人员编号由人员服务生成 */
|
||||
private LockPerson createOpenApiPerson(String appCode, String externalPersonId, String personName,
|
||||
OpenStayCheckinRequest request)
|
||||
{
|
||||
LockPerson person = new LockPerson();
|
||||
person.setPersonName(personName);
|
||||
person.setPersonType(StringUtils.isEmpty(StringUtils.trim(request.getPersonType())) ? PERSON_TYPE_VISITOR
|
||||
: StringUtils.trim(request.getPersonType()));
|
||||
person.setPhone(StringUtils.trim(request.getPhone()));
|
||||
person.setIdCardNo(StringUtils.trim(request.getIdCardNo()));
|
||||
person.setStatus(LockConstants.PERSON_STATUS_NORMAL);
|
||||
person.setCreateBy(LockConstants.CREATE_BY_OPENAPI + ":" + appCode);
|
||||
person.setRemark("开放接口建档(应用 " + appCode + ",外部标识 " + externalPersonId + ")");
|
||||
personService.insertPerson(person);
|
||||
return person;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把外部标识映射改指到新档案(退房清理后重新入住)。
|
||||
* <p>
|
||||
* 条件更新带旧人员编号:并发重新入住时只有一方改指成功,另一方回查后改用现有人员,
|
||||
* 其新建的档案与首次建档撞键的处理一致,留待人工合并。
|
||||
*/
|
||||
private String rebindPersonRef(String appCode, String externalPersonId, LockPersonRef ref, LockPerson person)
|
||||
{
|
||||
int rows = personRefMapper.rebindPerson(ref.getRefId(), ref.getPersonNo(), person.getPersonId(),
|
||||
person.getPersonNo());
|
||||
if (rows > 0)
|
||||
{
|
||||
log.info("外部人员重新建档并改指映射, appCode={}, externalId={}, oldPersonNo={}, newPersonNo={}", appCode,
|
||||
externalPersonId, ref.getPersonNo(), person.getPersonNo());
|
||||
return person.getPersonNo();
|
||||
}
|
||||
LockPersonRef winner = personRefMapper.selectByAppAndExternalId(appCode, externalPersonId);
|
||||
if (winner == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.INTERNAL_ERROR, "人员映射更新冲突,请重试");
|
||||
}
|
||||
log.warn("外部人员重新建档并发冲突, appCode={}, externalId={}, 本次建档={}, 已映射={}", appCode, externalPersonId,
|
||||
person.getPersonNo(), winner.getPersonNo());
|
||||
return winner.getPersonNo();
|
||||
}
|
||||
|
||||
private LockPerson requireMappedPerson(String personNo, String externalPersonId)
|
||||
{
|
||||
LockPerson person = personService.selectPersonByNo(personNo);
|
||||
if (person == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PERSON_INVALID,
|
||||
"外部标识对应的人员档案不存在,请联系平台核查,externalPersonId=" + externalPersonId);
|
||||
}
|
||||
return person;
|
||||
}
|
||||
|
||||
/** 停用或不在有效期的人员不允许办理入住 */
|
||||
private void validatePersonUsable(LockPerson person)
|
||||
{
|
||||
if (!LockConstants.PERSON_STATUS_NORMAL.equals(person.getStatus()))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PERSON_INVALID, "人员已停用,无法办理入住:" + person.getPersonNo());
|
||||
}
|
||||
Date now = new Date();
|
||||
if (person.getValidFrom() != null && person.getValidFrom().after(now))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PERSON_INVALID,
|
||||
"人员尚未到生效时间,无法办理入住:" + person.getPersonNo());
|
||||
}
|
||||
if (person.getValidTo() != null && person.getValidTo().before(now))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PERSON_INVALID, "人员已过有效期,无法办理入住:" + person.getPersonNo());
|
||||
}
|
||||
}
|
||||
|
||||
/** 写入外部标识映射;并发撞唯一键时以已存在的映射为准,返回其 personNo */
|
||||
private String savePersonRef(String appCode, String externalPersonId, LockPerson person)
|
||||
{
|
||||
LockPersonRef ref = new LockPersonRef();
|
||||
ref.setAppCode(appCode);
|
||||
ref.setExternalId(externalPersonId);
|
||||
ref.setPersonId(person.getPersonId());
|
||||
ref.setPersonNo(person.getPersonNo());
|
||||
ref.setCreateBy(LockConstants.CREATE_BY_OPENAPI + ":" + appCode);
|
||||
ref.setRemark("开放接口入住自动建档");
|
||||
try
|
||||
{
|
||||
personRefMapper.insertRef(ref);
|
||||
return person.getPersonNo();
|
||||
}
|
||||
catch (DuplicateKeyException e)
|
||||
{
|
||||
LockPersonRef winner = personRefMapper.selectByAppAndExternalId(appCode, externalPersonId);
|
||||
if (winner == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.INTERNAL_ERROR, "人员映射写入冲突,请重试");
|
||||
}
|
||||
log.warn("外部人员映射并发冲突, appCode={}, externalId={}, 本次建档={}, 已映射={}", appCode, externalPersonId,
|
||||
person.getPersonNo(), winner.getPersonNo());
|
||||
return winner.getPersonNo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保人员在目标门锁算法版本下有可用特征。
|
||||
* <p>
|
||||
* 照片未变且已有该版本特征时直接复用,避免每次入住都打一次提取服务;
|
||||
* 其余情况按门锁版本重新提取,提取失败即拒绝受理(不下发一份错误特征)。
|
||||
*/
|
||||
private void ensureFaceFeature(LockPerson person, OpenStayCheckinRequest request, LockDevice device)
|
||||
{
|
||||
String version = StringUtils.trim(device.getFaceAlgoVersion());
|
||||
// 入住必须带照片(入口已校验),这里直接落库或更新,不再沿用历史照片
|
||||
String photoUrl = resolvePhotoUrl(request);
|
||||
LockFace face = faceService.selectFaceByPersonNo(person.getPersonNo());
|
||||
boolean photoChanged;
|
||||
if (face == null)
|
||||
{
|
||||
face = new LockFace();
|
||||
face.setPersonNo(person.getPersonNo());
|
||||
face.setPersonName(person.getPersonName());
|
||||
face.setPhotoUrl(photoUrl);
|
||||
face.setStatus(FACE_STATUS_NORMAL);
|
||||
face.setCreateBy(LockConstants.CREATE_BY_OPENAPI);
|
||||
faceService.insertFace(face);
|
||||
photoChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
photoChanged = !photoUrl.equals(face.getPhotoUrl());
|
||||
if (photoChanged)
|
||||
{
|
||||
LockFace update = new LockFace();
|
||||
update.setFaceId(face.getFaceId());
|
||||
update.setPhotoUrl(photoUrl);
|
||||
update.setUpdateBy(LockConstants.CREATE_BY_OPENAPI);
|
||||
faceService.updateFace(update);
|
||||
face.setPhotoUrl(photoUrl);
|
||||
}
|
||||
}
|
||||
LockFaceFeature feature = faceService.selectFeature(person.getPersonNo(), version);
|
||||
boolean usable = feature != null && StringUtils.isNotEmpty(feature.getFeatureData());
|
||||
if (usable && !photoChanged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String extracted = faceService.extract(face.getFaceId(), device.getDeviceId());
|
||||
if (StringUtils.isNotEmpty(extracted))
|
||||
{
|
||||
return;
|
||||
}
|
||||
LockFace latest = faceService.selectFaceById(face.getFaceId());
|
||||
String reason = latest == null ? null : latest.getExtractMsg();
|
||||
if (StringUtils.isEmpty(version))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.FACE_EXTRACT_FAIL,
|
||||
"门锁未配置人脸算法版本(face_algo_version),请先在门锁页维护后再办理入住,deviceSn="
|
||||
+ device.getDeviceSn());
|
||||
}
|
||||
throw new OpenApiException(OpenApiCodes.FACE_EXTRACT_FAIL, "人脸特征提取失败(门锁算法版本 " + version + "):"
|
||||
+ (StringUtils.isEmpty(reason) ? "请检查照片质量后重试" : reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* 头像来源:photoUrl 直接用,photoBase64 落盘后转成平台内部地址。
|
||||
* 入住入口已校验两者至少一个非空,这里不会再返回 null。
|
||||
*/
|
||||
private String resolvePhotoUrl(OpenStayCheckinRequest request)
|
||||
{
|
||||
String photoUrl = StringUtils.trim(request.getPhotoUrl());
|
||||
if (StringUtils.isNotEmpty(photoUrl))
|
||||
{
|
||||
return photoUrl;
|
||||
}
|
||||
String base64 = StringUtils.trim(request.getPhotoBase64());
|
||||
if (StringUtils.isEmpty(base64))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return saveBase64Photo(base64);
|
||||
}
|
||||
|
||||
/**
|
||||
* base64 头像落盘:文件名由平台生成(不含调用方输入),只写 profile 目录下的
|
||||
* openapi 子目录,返回 {@code /profile/...} 相对地址供特征提取读取。
|
||||
*/
|
||||
private String saveBase64Photo(String base64)
|
||||
{
|
||||
String raw = base64;
|
||||
if (raw.startsWith("data:"))
|
||||
{
|
||||
int comma = raw.indexOf(',');
|
||||
if (comma < 0)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "photoBase64 的 data URL 格式不正确");
|
||||
}
|
||||
raw = raw.substring(comma + 1);
|
||||
}
|
||||
byte[] bytes;
|
||||
try
|
||||
{
|
||||
bytes = Base64.getDecoder().decode(raw.trim());
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "photoBase64 不是合法的 base64 编码");
|
||||
}
|
||||
if (bytes.length == 0)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "photoBase64 解码后为空");
|
||||
}
|
||||
if (bytes.length > PHOTO_MAX_BYTES)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "photoBase64 解码后不能超过 2MB");
|
||||
}
|
||||
String extension = imageExtension(bytes);
|
||||
String relative = "/openapi/" + DateUtils.dateTimeNow("yyyy/MM/dd") + "/" + IdUtils.fastSimpleUUID()
|
||||
+ extension;
|
||||
try
|
||||
{
|
||||
// 与 LockFaceServiceImpl.loadImageBytes 的读取方式保持一致:profile + 相对路径
|
||||
Path target = Paths.get(RuoYiConfig.getProfile() + relative);
|
||||
Files.createDirectories(target.getParent());
|
||||
Files.write(target, bytes);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
log.error("开放接口头像落盘失败, relative={}", relative, e);
|
||||
throw new OpenApiException(OpenApiCodes.INTERNAL_ERROR, "头像保存失败,请重试或改用 photoUrl");
|
||||
}
|
||||
return Constants.RESOURCE_PREFIX + relative;
|
||||
}
|
||||
|
||||
/** 按文件头判定图片类型,只接受 JPG/PNG */
|
||||
private String imageExtension(byte[] bytes)
|
||||
{
|
||||
if (bytes.length > 3 && (bytes[0] & 0xFF) == 0xFF && (bytes[1] & 0xFF) == 0xD8 && (bytes[2] & 0xFF) == 0xFF)
|
||||
{
|
||||
return ".jpg";
|
||||
}
|
||||
if (bytes.length > 4 && (bytes[0] & 0xFF) == 0x89 && bytes[1] == 'P' && bytes[2] == 'N' && bytes[3] == 'G')
|
||||
{
|
||||
return ".png";
|
||||
}
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "photoBase64 仅支持 JPG/PNG 图片");
|
||||
}
|
||||
|
||||
/** 房号全局唯一:查不到或查到多条都按 40003 处理,避免误选房间 */
|
||||
private LockRoom requireUniqueRoom(String roomNo)
|
||||
{
|
||||
List<LockRoom> rooms = roomMapper.selectRoomsByRoomNo(roomNo);
|
||||
if (rooms == null || rooms.isEmpty())
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.ROOM_NOT_FOUND, "房间不存在:" + roomNo);
|
||||
}
|
||||
if (rooms.size() > 1)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.ROOM_NOT_FOUND,
|
||||
"房间号不唯一(" + rooms.size() + " 条),请联系平台核对房间档案:" + roomNo);
|
||||
}
|
||||
return rooms.get(0);
|
||||
}
|
||||
|
||||
/** 不限制入住人数,只拒绝维修/停用房间 */
|
||||
private void requireCheckinable(LockRoom room)
|
||||
{
|
||||
if (ROOM_STATUS_REPAIR.equals(room.getRoomStatus()) || ROOM_STATUS_DISABLED.equals(room.getRoomStatus()))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.ROOM_UNAVAILABLE,
|
||||
"房间处于维修/停用状态,不可办理入住:" + room.getRoomNo());
|
||||
}
|
||||
}
|
||||
|
||||
private LockDevice requireRoomDevice(LockRoom room)
|
||||
{
|
||||
if (room.getDeviceId() == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.ROOM_UNAVAILABLE,
|
||||
"房间未分配门锁,请联系平台先绑定门锁:" + room.getRoomNo());
|
||||
}
|
||||
LockDevice device = deviceMapper.selectDeviceById(room.getDeviceId());
|
||||
if (device == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.ROOM_UNAVAILABLE, "房间绑定的门锁不存在:" + room.getRoomNo());
|
||||
}
|
||||
return device;
|
||||
}
|
||||
|
||||
/** 人员标识解析:personNo 优先,其次外部标识查映射;映射缺失按「入住单不存在」处理 */
|
||||
private String resolvePersonNo(String appCode, String personNo, String externalPersonId)
|
||||
{
|
||||
String no = StringUtils.trim(personNo);
|
||||
if (StringUtils.isNotEmpty(no))
|
||||
{
|
||||
return no;
|
||||
}
|
||||
String external = StringUtils.trim(externalPersonId);
|
||||
if (StringUtils.isEmpty(external))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, "bizNo 或「personNo/externalPersonId + roomNo」必须提供其一");
|
||||
}
|
||||
LockPersonRef ref = personRefMapper.selectByAppAndExternalId(appCode, external);
|
||||
if (ref == null)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.STAY_NOT_FOUND,
|
||||
"外部人员标识未在本应用建档,externalPersonId=" + external);
|
||||
}
|
||||
return ref.getPersonNo();
|
||||
}
|
||||
|
||||
private boolean hasActiveStayInRoom(String personNo, Long roomId)
|
||||
{
|
||||
for (LockStay stay : stayMapper.selectActiveStaysByRoomId(roomId))
|
||||
{
|
||||
if (personNo.equals(stay.getPersonNo()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private OpenStayResult toResult(LockStay stay, String appCode)
|
||||
{
|
||||
OpenStayResult result = new OpenStayResult();
|
||||
result.setBizNo(stay.getBizNo());
|
||||
result.setStayNo(stay.getStayNo());
|
||||
result.setPersonNo(stay.getPersonNo());
|
||||
result.setRoomCode(stay.getRoomCode());
|
||||
result.setRoomNo(stay.getRoomNo());
|
||||
result.setDeviceSn(stay.getDeviceSn());
|
||||
result.setBeginTime(stay.getBeginTime());
|
||||
result.setEndTime(stay.getEndTime());
|
||||
result.setStayStatus(stay.getStayStatus());
|
||||
result.setAuthStatus(stay.getAuthStatus());
|
||||
result.setAccepted(Boolean.TRUE);
|
||||
App app = openAppRegistry.getApp(appCode);
|
||||
if (app != null)
|
||||
{
|
||||
boolean enabled = StringUtils.isNotEmpty(StringUtils.trim(app.getCallbackUrl()));
|
||||
result.setCallback(new OpenStayResult.Callback(enabled, enabled ? app.getCallbackUrl().trim() : null));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 住宿服务的可读异常按对外业务码归类。
|
||||
* 房间/门锁类归 40004、人员类归 40005、特征类归 50001,其余按参数错误 40001。
|
||||
*/
|
||||
private OpenApiException translateStayError(String message)
|
||||
{
|
||||
String msg = StringUtils.isEmpty(message) ? "住宿业务处理失败" : message;
|
||||
if (msg.contains("人脸特征") || msg.contains("照片"))
|
||||
{
|
||||
return new OpenApiException(OpenApiCodes.FACE_EXTRACT_FAIL, msg);
|
||||
}
|
||||
if (msg.contains("人员"))
|
||||
{
|
||||
return new OpenApiException(OpenApiCodes.PERSON_INVALID, msg);
|
||||
}
|
||||
if (msg.contains("房间") || msg.contains("门锁"))
|
||||
{
|
||||
return new OpenApiException(OpenApiCodes.ROOM_UNAVAILABLE, msg);
|
||||
}
|
||||
return new OpenApiException(OpenApiCodes.PARAM_INVALID, msg);
|
||||
}
|
||||
|
||||
private String requireText(String value, String field)
|
||||
{
|
||||
String text = StringUtils.trim(value);
|
||||
if (StringUtils.isEmpty(text))
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, field + " 不能为空");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private Date parseTime(String value, String field)
|
||||
{
|
||||
String text = requireText(value, field);
|
||||
SimpleDateFormat format = new SimpleDateFormat(TIME_PATTERN);
|
||||
format.setLenient(false);
|
||||
try
|
||||
{
|
||||
return format.parse(text);
|
||||
}
|
||||
catch (ParseException e)
|
||||
{
|
||||
throw new OpenApiException(OpenApiCodes.PARAM_INVALID, field + " 时间格式不正确,应为 " + TIME_PATTERN);
|
||||
}
|
||||
}
|
||||
|
||||
private String formatTime(Date date)
|
||||
{
|
||||
return date == null ? null : new SimpleDateFormat(TIME_PATTERN).format(date);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.ruoyi.iot.person.service;
|
||||
|
||||
import java.util.List;
|
||||
import com.ruoyi.iot.person.domain.LockPerson;
|
||||
|
||||
/**
|
||||
* 人员主数据服务
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface ILockPersonService
|
||||
{
|
||||
List<LockPerson> selectPersonList(LockPerson person);
|
||||
|
||||
/** 下拉选项:仅正常状态人员 */
|
||||
List<LockPerson> selectPersonOption();
|
||||
|
||||
LockPerson selectPersonById(Long personId);
|
||||
|
||||
LockPerson selectPersonByNo(String personNo);
|
||||
|
||||
int insertPerson(LockPerson person);
|
||||
|
||||
int updatePerson(LockPerson person);
|
||||
|
||||
int deletePersonByIds(Long[] personIds);
|
||||
|
||||
/**
|
||||
* 直接删除人员记录(物理删除),跳过人脸/授权引用校验。
|
||||
* <p>
|
||||
* 供退房清理外部接口自动建档人员使用:调用方已确认该人员无其它在住单与有效授权,
|
||||
* 人脸与特征由调用方先行删除。人工删除仍走 {@link #deletePersonByIds(Long[])}。
|
||||
*
|
||||
* @param personId 人员ID
|
||||
* @return 受影响行数
|
||||
*/
|
||||
int deletePersonRecord(Long personId);
|
||||
|
||||
/**
|
||||
* 生成人员编号:2 位字母前缀 + 1 位人员类型 + 5 位序列(如 AA000001)
|
||||
* <p>
|
||||
* v1 人员不挂分区,前缀固定 AA;分区维度落地后由分区决定前缀。
|
||||
*
|
||||
* @param personType 人员类型 0-3,为空按 0 处理
|
||||
* @return 8 位人员编号
|
||||
*/
|
||||
String nextPersonNo(String personType);
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
package com.ruoyi.iot.person.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.iot.lock.LockConstants;
|
||||
import com.ruoyi.iot.lock.mapper.LockAuthMapper;
|
||||
import com.ruoyi.iot.lock.mapper.LockFaceMapper;
|
||||
import com.ruoyi.iot.person.domain.LockPerson;
|
||||
import com.ruoyi.iot.person.mapper.LockPersonMapper;
|
||||
import com.ruoyi.iot.person.service.ILockPersonService;
|
||||
|
||||
/**
|
||||
* 人员主数据服务实现
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class LockPersonServiceImpl implements ILockPersonService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(LockPersonServiceImpl.class);
|
||||
|
||||
/** 人员编号:2 位字母 + 6 位数字,与设备协议 / 开门记录解析保持一致 */
|
||||
private static final Pattern PERSON_NO_PATTERN = Pattern.compile("^[A-Z]{2}\\d{6}$");
|
||||
|
||||
/** v1 人员不挂分区,编号前缀固定 */
|
||||
private static final String PERSON_NO_PREFIX = "AA";
|
||||
|
||||
private static final String PERSON_NO_SEQ_KEY = "lock:person:no:";
|
||||
|
||||
private static final int PERSON_NO_MAX_SEQ = 99999;
|
||||
|
||||
@Autowired
|
||||
private LockPersonMapper personMapper;
|
||||
|
||||
@Autowired
|
||||
private LockFaceMapper faceMapper;
|
||||
|
||||
@Autowired
|
||||
private LockAuthMapper authMapper;
|
||||
|
||||
@Autowired(required = false)
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Override
|
||||
public List<LockPerson> selectPersonList(LockPerson person)
|
||||
{
|
||||
return personMapper.selectPersonList(person);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LockPerson> selectPersonOption()
|
||||
{
|
||||
return personMapper.selectPersonOption();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockPerson selectPersonById(Long personId)
|
||||
{
|
||||
return personMapper.selectPersonById(personId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LockPerson selectPersonByNo(String personNo)
|
||||
{
|
||||
if (StringUtils.isEmpty(StringUtils.trim(personNo)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return personMapper.selectPersonByNo(StringUtils.trim(personNo));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertPerson(LockPerson person)
|
||||
{
|
||||
if (person == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人员信息不能为空");
|
||||
}
|
||||
String personNo = StringUtils.trim(person.getPersonNo());
|
||||
if (StringUtils.isEmpty(personNo))
|
||||
{
|
||||
personNo = nextPersonNo(person.getPersonType());
|
||||
}
|
||||
person.setPersonNo(personNo);
|
||||
validatePersonNo(personNo);
|
||||
person.setPersonType(normalizePersonType(person.getPersonType()));
|
||||
validateValidRange(person.getValidFrom(), person.getValidTo());
|
||||
if (personMapper.countPersonNo(personNo, null) > 0)
|
||||
{
|
||||
throw new IllegalArgumentException("人员编号已存在:" + personNo);
|
||||
}
|
||||
if (StringUtils.isEmpty(person.getStatus()))
|
||||
{
|
||||
person.setStatus(LockConstants.PERSON_STATUS_NORMAL);
|
||||
}
|
||||
return personMapper.insertPerson(person);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updatePerson(LockPerson person)
|
||||
{
|
||||
if (person == null || person.getPersonId() == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人员ID不能为空");
|
||||
}
|
||||
LockPerson old = personMapper.selectPersonById(person.getPersonId());
|
||||
if (old == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人员不存在");
|
||||
}
|
||||
// person_no 已随授权下发到设备,设备侧用它索引特征与权限,改号等于换成另一个人
|
||||
String personNo = StringUtils.trim(person.getPersonNo());
|
||||
if (StringUtils.isNotEmpty(personNo) && !personNo.equals(old.getPersonNo()))
|
||||
{
|
||||
throw new IllegalArgumentException("人员编号已下发到设备,不可变更");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(person.getPersonType()))
|
||||
{
|
||||
person.setPersonType(normalizePersonType(person.getPersonType()));
|
||||
}
|
||||
Date validFrom = person.getValidFrom() == null ? old.getValidFrom() : person.getValidFrom();
|
||||
Date validTo = person.getValidTo() == null ? old.getValidTo() : person.getValidTo();
|
||||
validateValidRange(validFrom, validTo);
|
||||
return personMapper.updatePerson(person);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int deletePersonByIds(Long[] personIds)
|
||||
{
|
||||
for (Long personId : personIds)
|
||||
{
|
||||
LockPerson person = personMapper.selectPersonById(personId);
|
||||
if (person == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int faces = faceMapper.countFaceByPersonId(personId);
|
||||
if (faces > 0)
|
||||
{
|
||||
throw new IllegalArgumentException("人员 " + person.getPersonNo() + " 已有人脸特征,请先删除人脸数据");
|
||||
}
|
||||
int auths = authMapper.countAuthsByPersonNo(person.getPersonNo());
|
||||
if (auths > 0)
|
||||
{
|
||||
throw new IllegalArgumentException(
|
||||
"人员 " + person.getPersonNo() + " 仍有 " + auths + " 条授权,请先撤销授权");
|
||||
}
|
||||
}
|
||||
return personMapper.deletePersonByIds(personIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deletePersonRecord(Long personId)
|
||||
{
|
||||
if (personId == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return personMapper.deletePersonByIds(new Long[] { personId });
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextPersonNo(String personType)
|
||||
{
|
||||
String type = normalizePersonType(personType);
|
||||
if (redisCache == null)
|
||||
{
|
||||
throw new IllegalArgumentException("人员编号自动生成不可用,请手工填写人员编号");
|
||||
}
|
||||
String seqKey = PERSON_NO_SEQ_KEY + PERSON_NO_PREFIX + type;
|
||||
try
|
||||
{
|
||||
Long seq = (Long) redisCache.redisTemplate.opsForValue().increment(seqKey);
|
||||
if (seq == null || seq > PERSON_NO_MAX_SEQ)
|
||||
{
|
||||
throw new IllegalArgumentException("人员编号序列已用尽,请手工填写人员编号");
|
||||
}
|
||||
return PERSON_NO_PREFIX + type + String.format("%05d", seq);
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("人员编号生成失败, type={}, msg={}", type, e.getMessage());
|
||||
throw new IllegalArgumentException("人员编号自动生成失败,请手工填写人员编号");
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePersonNo(String personNo)
|
||||
{
|
||||
if (!PERSON_NO_PATTERN.matcher(personNo).matches())
|
||||
{
|
||||
throw new IllegalArgumentException("人员编号格式错误,须为 2 位字母 + 6 位数字(如 AA000003):" + personNo);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizePersonType(String personType)
|
||||
{
|
||||
String type = StringUtils.isEmpty(StringUtils.trim(personType)) ? "0" : StringUtils.trim(personType);
|
||||
if (!"0".equals(type) && !"1".equals(type) && !"2".equals(type) && !"3".equals(type))
|
||||
{
|
||||
throw new IllegalArgumentException("人员类型不合法:" + personType);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private void validateValidRange(Date validFrom, Date validTo)
|
||||
{
|
||||
if (validFrom != null && validTo != null && validFrom.after(validTo))
|
||||
{
|
||||
throw new IllegalArgumentException("有效期起不能晚于有效期止");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.iot.lock.mapper.LockAuthMapper">
|
||||
|
||||
<resultMap type="LockAuth" id="LockAuthResult">
|
||||
<id property="authId" column="auth_id" />
|
||||
<result property="authNo" column="auth_no" />
|
||||
<result property="personNo" column="person_no" />
|
||||
<result property="personName" column="person_name" />
|
||||
<result property="deviceId" column="device_id" />
|
||||
<result property="deviceSn" column="device_sn" />
|
||||
<result property="gatewayId" column="gateway_id" />
|
||||
<result property="groupId" column="group_id" />
|
||||
<result property="authType" column="auth_type" />
|
||||
<result property="authValue" column="auth_value" />
|
||||
<result property="authValueMd5" column="auth_value_md5" />
|
||||
<result property="beginTime" column="begin_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="authTime" column="auth_time" />
|
||||
<result property="syncStatus" column="sync_status" />
|
||||
<result property="syncTime" column="sync_time" />
|
||||
<result property="syncMsg" column="sync_msg" />
|
||||
<result property="retryCount" column="retry_count" />
|
||||
<result property="status" column="status" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</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,
|
||||
sync_time, sync_msg, retry_count, status, del_flag, create_by, create_time,
|
||||
update_by, update_time, remark from lock_auth
|
||||
</sql>
|
||||
|
||||
<select id="selectAuthList" parameterType="LockAuth" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
<where>
|
||||
del_flag = '0'
|
||||
<if test="authNo != null and authNo != ''">
|
||||
and auth_no like concat('%', #{authNo}, '%')
|
||||
</if>
|
||||
<if test="personNo != null and personNo != ''">
|
||||
and person_no like concat('%', #{personNo}, '%')
|
||||
</if>
|
||||
<if test="personName != null and personName != ''">
|
||||
and person_name like concat('%', #{personName}, '%')
|
||||
</if>
|
||||
<if test="deviceId != null">
|
||||
and device_id = #{deviceId}
|
||||
</if>
|
||||
<if test="deviceSn != null and deviceSn != ''">
|
||||
and device_sn = #{deviceSn}
|
||||
</if>
|
||||
<if test="groupId != null">
|
||||
and group_id = #{groupId}
|
||||
</if>
|
||||
<if test="authType != null and authType != ''">
|
||||
and auth_type = #{authType}
|
||||
</if>
|
||||
<if test="syncStatus != null and syncStatus != ''">
|
||||
and sync_status = #{syncStatus}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
and status = #{status}
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and date_format(create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and date_format(create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
order by auth_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectAuthById" parameterType="Long" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where auth_id = #{authId}
|
||||
</select>
|
||||
|
||||
<select id="selectAuthsByDeviceId" parameterType="Long" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where device_id = #{deviceId} and del_flag = '0'
|
||||
order by auth_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectAuthsByPersonNo" parameterType="String" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where person_no = #{personNo} and del_flag = '0'
|
||||
order by auth_id desc
|
||||
</select>
|
||||
|
||||
<select id="countAuthsByPersonNo" parameterType="String" resultType="int">
|
||||
select count(1) from lock_auth where person_no = #{personNo} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 已清除(4)不再有设备侧权限;3/6 保留:前者仍可能被补偿重发,后者人脸还在设备上 -->
|
||||
<select id="countActiveAuthsByPersonNo" resultType="int">
|
||||
select count(1) from lock_auth
|
||||
where person_no = #{personNo} and del_flag = '0'
|
||||
and (sync_status is null or sync_status != '4')
|
||||
<if test="excludeAuthId != null">
|
||||
and auth_id != #{excludeAuthId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!-- status 为 null 视为正常:历史数据由 insert 显式写 null 产生,不能因此漏掉补偿 -->
|
||||
<select id="selectPendingAuths" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where del_flag = '0' and (status = '0' or status is null) and sync_status in ('0', '3')
|
||||
<if test="groupId != null">
|
||||
and group_id = #{groupId}
|
||||
</if>
|
||||
order by auth_id
|
||||
</select>
|
||||
|
||||
<!-- 清除失败(6):只重发 delete;带 5 分钟退避,避免门锁长期离线时每轮补偿都建命令 -->
|
||||
<select id="selectPendingClearAuths" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where del_flag = '0' and (status = '0' or status is null) and sync_status = '6'
|
||||
and (sync_time is null or sync_time <= date_sub(now(), interval 5 minute))
|
||||
order by auth_id
|
||||
</select>
|
||||
|
||||
<select id="selectFaceAlgoVersionsByPersonNo" parameterType="String" resultType="String">
|
||||
select distinct trim(d.face_algo_version)
|
||||
from lock_auth a
|
||||
join lock_device d on d.device_id = a.device_id
|
||||
where a.person_no = #{personNo}
|
||||
and a.auth_type = '01'
|
||||
and a.del_flag = '0'
|
||||
and d.del_flag = '0'
|
||||
and d.face_algo_version is not null
|
||||
and trim(d.face_algo_version) <> ''
|
||||
order by trim(d.face_algo_version)
|
||||
</select>
|
||||
|
||||
<select id="selectFaceAuthsForRepush" resultMap="LockAuthResult">
|
||||
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.auth_value, a.auth_value_md5, a.begin_time, a.end_time,
|
||||
a.auth_time, 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
|
||||
from lock_auth a
|
||||
join lock_device d on d.device_id = a.device_id
|
||||
where a.person_no = #{personNo}
|
||||
and a.auth_type = '01'
|
||||
and a.del_flag = '0'
|
||||
and a.sync_status in ('0', '3')
|
||||
and d.del_flag = '0'
|
||||
and trim(coalesce(d.face_algo_version, '')) = #{algoVersion}
|
||||
order by a.auth_id
|
||||
</select>
|
||||
|
||||
<!-- 唯一键 (person_no, device_id, auth_type) 不含 del_flag,逻辑删除行仍占用键值,不过滤 -->
|
||||
<select id="selectAuthByUniqueKey" resultMap="LockAuthResult">
|
||||
<include refid="selectAuthVo"/>
|
||||
where person_no = #{personNo} and device_id = #{deviceId} and auth_type = #{authType}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 同人同锁同类型只保留一行:重复新增或删除后重新添加时复用原行并重置下发状态 -->
|
||||
<update id="reviveAuth" parameterType="LockAuth">
|
||||
update lock_auth
|
||||
set person_name = #{personName},
|
||||
device_sn = #{deviceSn},
|
||||
gateway_id = #{gatewayId},
|
||||
group_id = #{groupId},
|
||||
auth_value = #{authValue},
|
||||
auth_value_md5 = #{authValueMd5},
|
||||
begin_time = #{beginTime},
|
||||
end_time = #{endTime},
|
||||
auth_time = #{authTime},
|
||||
remark = #{remark},
|
||||
status = #{status},
|
||||
del_flag = '0',
|
||||
sync_status = '0',
|
||||
sync_time = null,
|
||||
sync_msg = null,
|
||||
retry_count = 0,
|
||||
update_by = #{updateBy},
|
||||
update_time = CURRENT_TIMESTAMP
|
||||
where auth_id = #{authId}
|
||||
</update>
|
||||
|
||||
<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,
|
||||
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}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateAuth" parameterType="LockAuth">
|
||||
update lock_auth
|
||||
<set>
|
||||
<if test="personNo != null and personNo != ''">person_no = #{personNo},</if>
|
||||
<if test="personName != null">person_name = #{personName},</if>
|
||||
<if test="deviceId != null">device_id = #{deviceId},</if>
|
||||
<if test="deviceSn != null">device_sn = #{deviceSn},</if>
|
||||
<if test="gatewayId != null">gateway_id = #{gatewayId},</if>
|
||||
<if test="groupId != null">group_id = #{groupId},</if>
|
||||
<if test="authType != null and authType != ''">auth_type = #{authType},</if>
|
||||
<if test="authValue != null">auth_value = #{authValue},</if>
|
||||
<if test="authValueMd5 != null">auth_value_md5 = #{authValueMd5},</if>
|
||||
<if test="beginTime != null">begin_time = #{beginTime},</if>
|
||||
<if test="endTime != null">end_time = #{endTime},</if>
|
||||
<if test="authTime != null">auth_time = #{authTime},</if>
|
||||
<if test="status != null and status != ''">status = #{status},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP
|
||||
</set>
|
||||
where auth_id = #{authId}
|
||||
</update>
|
||||
|
||||
<update id="updateSyncStatus">
|
||||
update lock_auth set sync_status = #{syncStatus}, sync_time = CURRENT_TIMESTAMP
|
||||
<if test="syncMsg != null">, sync_msg = #{syncMsg}</if>
|
||||
where auth_id = #{authId}
|
||||
</update>
|
||||
|
||||
<update id="markFaceAuthsForReextract">
|
||||
update lock_auth
|
||||
set sync_status = '3', sync_time = CURRENT_TIMESTAMP, sync_msg = #{syncMsg}
|
||||
where device_id = #{deviceId}
|
||||
and auth_type = '01'
|
||||
and del_flag = '0'
|
||||
and sync_status not in ('4', '6')
|
||||
</update>
|
||||
|
||||
<update id="deleteAuthByIds" parameterType="Long">
|
||||
update lock_auth set del_flag = '2' where auth_id in
|
||||
<foreach collection="array" item="authId" open="(" separator="," close=")">
|
||||
#{authId}
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<delete id="deleteAuthByDeviceId" parameterType="Long">
|
||||
delete from lock_auth where device_id = #{deviceId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.iot.lock.mapper.LockFaceMapper">
|
||||
|
||||
<resultMap type="LockFace" id="LockFaceResult">
|
||||
<id property="faceId" column="face_id" />
|
||||
<result property="personId" column="person_id" />
|
||||
<result property="personNo" column="person_no" />
|
||||
<result property="personName" column="person_name" />
|
||||
<result property="personType" column="person_type" />
|
||||
<result property="phone" column="phone" />
|
||||
<result property="photoUrl" column="photo_url" />
|
||||
<result property="photoMd5" column="photo_md5" />
|
||||
<result property="featureData" column="feature_data" />
|
||||
<result property="featureMd5" column="feature_md5" />
|
||||
<result property="featureAlgoVersion" column="feature_algo_version" />
|
||||
<result property="featureSize" column="feature_size" />
|
||||
<result property="extractStatus" column="extract_status" />
|
||||
<result property="extractTime" column="extract_time" />
|
||||
<result property="extractMsg" column="extract_msg" />
|
||||
<result property="status" column="status" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<!-- 人员维度只在 lock_person 维护,本表通过 person_id 关联读取 -->
|
||||
<sql id="selectFaceListVo">
|
||||
select f.face_id, f.person_id, f.person_no, p.person_name, p.person_type, p.phone,
|
||||
f.photo_url, f.photo_md5, f.feature_md5, f.feature_algo_version, f.feature_size,
|
||||
f.extract_status, f.extract_time, f.extract_msg, f.status, f.del_flag,
|
||||
f.create_by, f.create_time, f.update_by, f.update_time, f.remark
|
||||
from lock_face f
|
||||
left join lock_person p on p.person_id = f.person_id
|
||||
</sql>
|
||||
|
||||
<sql id="selectFaceVo">
|
||||
select f.face_id, f.person_id, f.person_no, p.person_name, p.person_type, p.phone,
|
||||
f.photo_url, f.photo_md5, f.feature_data, f.feature_md5, f.feature_algo_version,
|
||||
f.feature_size, f.extract_status, f.extract_time, f.extract_msg, f.status, f.del_flag,
|
||||
f.create_by, f.create_time, f.update_by, f.update_time, f.remark
|
||||
from lock_face f
|
||||
left join lock_person p on p.person_id = f.person_id
|
||||
</sql>
|
||||
|
||||
<!-- 列表不返回 feature_data,避免大字段传输 -->
|
||||
<select id="selectFaceList" parameterType="LockFace" resultMap="LockFaceResult">
|
||||
<include refid="selectFaceListVo"/>
|
||||
<where>
|
||||
f.del_flag = '0'
|
||||
<if test="personNo != null and personNo != ''">
|
||||
and f.person_no like concat('%', #{personNo}, '%')
|
||||
</if>
|
||||
<if test="personName != null and personName != ''">
|
||||
and p.person_name like concat('%', #{personName}, '%')
|
||||
</if>
|
||||
<if test="personType != null and personType != ''">
|
||||
and p.person_type = #{personType}
|
||||
</if>
|
||||
<if test="extractStatus != null and extractStatus != ''">
|
||||
and f.extract_status = #{extractStatus}
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
and f.status = #{status}
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and date_format(f.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and date_format(f.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
order by f.face_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectFaceById" parameterType="Long" resultMap="LockFaceResult">
|
||||
<include refid="selectFaceVo"/>
|
||||
where f.face_id = #{faceId}
|
||||
</select>
|
||||
|
||||
<select id="selectFaceByPersonNo" parameterType="String" resultMap="LockFaceResult">
|
||||
<include refid="selectFaceVo"/>
|
||||
where f.person_no = #{personNo} and f.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countFaceByPersonId" parameterType="Long" resultType="int">
|
||||
select count(1) from lock_face where person_id = #{personId} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countFaceByPhotoUrl" parameterType="String" resultType="int">
|
||||
select count(1) from lock_face where photo_url = #{photoUrl} and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 唯一键 uk_lock_face_person_id / uk_lock_face_person_no 均不含 del_flag:
|
||||
重新建档(退房清理、人工删人脸后再次入住)要先看历史行,命中即复用而不是再插一条 -->
|
||||
<select id="selectReusableFace" resultMap="LockFaceResult">
|
||||
<include refid="selectFaceListVo"/>
|
||||
where f.person_id = #{personId} or f.person_no = #{personNo}
|
||||
order by case when f.person_id = #{personId} then 0 else 1 end, f.face_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<update id="reviveFace" parameterType="LockFace">
|
||||
update lock_face
|
||||
set person_id = #{personId}, person_no = #{personNo}, photo_url = #{photoUrl},
|
||||
photo_md5 = #{photoMd5}, extract_status = '0', extract_time = null, extract_msg = null,
|
||||
feature_data = null, feature_md5 = null, feature_algo_version = null, feature_size = 0,
|
||||
status = #{status}, del_flag = '0', update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP
|
||||
where face_id = #{faceId}
|
||||
</update>
|
||||
|
||||
<insert id="insertFace" parameterType="LockFace" useGeneratedKeys="true" keyProperty="faceId">
|
||||
insert into lock_face (
|
||||
person_id, person_no, photo_url, photo_md5,
|
||||
extract_status, status, del_flag, create_by, create_time, remark
|
||||
) values (
|
||||
#{personId}, #{personNo}, #{photoUrl}, #{photoMd5},
|
||||
'0', #{status}, '0', #{createBy}, CURRENT_TIMESTAMP, #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateFace" parameterType="LockFace">
|
||||
update lock_face
|
||||
<set>
|
||||
<if test="photoUrl != null">photo_url = #{photoUrl},</if>
|
||||
<if test="photoMd5 != null">photo_md5 = #{photoMd5},</if>
|
||||
<if test="status != null and status != ''">status = #{status},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP
|
||||
</set>
|
||||
where face_id = #{faceId}
|
||||
</update>
|
||||
|
||||
<!-- 特征列只写非空值:提取失败不应该把上一次成功的镜像清空 -->
|
||||
<update id="updateExtractResult" parameterType="LockFace">
|
||||
update lock_face
|
||||
set extract_status = #{extractStatus}, extract_time = CURRENT_TIMESTAMP,
|
||||
extract_msg = #{extractMsg}
|
||||
<if test="featureData != null">, feature_data = #{featureData}</if>
|
||||
<if test="featureMd5 != null">, feature_md5 = #{featureMd5}</if>
|
||||
<if test="featureAlgoVersion != null">, feature_algo_version = #{featureAlgoVersion}</if>
|
||||
<if test="featureSize != null">, feature_size = #{featureSize}</if>
|
||||
where face_id = #{faceId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteFaceByIds" parameterType="Long">
|
||||
delete from lock_face where face_id in
|
||||
<foreach collection="array" item="faceId" open="(" separator="," close=")">
|
||||
#{faceId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.iot.lock.mapper.LockPersonRefMapper">
|
||||
|
||||
<resultMap type="LockPersonRef" id="LockPersonRefResult">
|
||||
<id property="refId" column="ref_id" />
|
||||
<result property="personId" column="person_id" />
|
||||
<result property="personNo" column="person_no" />
|
||||
<result property="appCode" column="app_code" />
|
||||
<result property="externalId" column="external_id" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByAppAndExternalId" resultMap="LockPersonRefResult">
|
||||
select ref_id, person_id, person_no, app_code, external_id, create_by, create_time, remark
|
||||
from lock_person_ref
|
||||
where app_code = #{appCode} and external_id = #{externalId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 回调用:同一人员在同一应用下只有一条映射,取最新一条防脏数据报错 -->
|
||||
<select id="selectByAppAndPersonNo" resultMap="LockPersonRefResult">
|
||||
select ref_id, person_id, person_no, app_code, external_id, create_by, create_time, remark
|
||||
from lock_person_ref
|
||||
where app_code = #{appCode} and person_no = #{personNo}
|
||||
order by ref_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertRef" parameterType="LockPersonRef" useGeneratedKeys="true" keyProperty="refId">
|
||||
insert into lock_person_ref (
|
||||
person_id, person_no, app_code, external_id, create_by, create_time, remark
|
||||
) values (
|
||||
#{personId}, #{personNo}, #{appCode}, #{externalId}, #{createBy}, CURRENT_TIMESTAMP, #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 旧人员编号做条件:同一外部标识并发重新入住时只有一方能改指成功 -->
|
||||
<update id="rebindPerson">
|
||||
update lock_person_ref
|
||||
set person_id = #{personId}, person_no = #{personNo}
|
||||
where ref_id = #{refId} and person_no = #{oldPersonNo}
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,237 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.iot.lock.mapper.LockStayMapper">
|
||||
|
||||
<resultMap type="LockStay" id="LockStayResult">
|
||||
<id property="stayId" column="stay_id" />
|
||||
<result property="stayNo" column="stay_no" />
|
||||
<result property="bizNo" column="biz_no" />
|
||||
<result property="bizSystem" column="biz_system" />
|
||||
<result property="source" column="source" />
|
||||
<result property="personId" column="person_id" />
|
||||
<result property="personNo" column="person_no" />
|
||||
<result property="personName" column="person_name" />
|
||||
<result property="guestType" column="guest_type" />
|
||||
<result property="roomId" column="room_id" />
|
||||
<result property="roomCode" column="room_code" />
|
||||
<result property="roomNo" column="room_no" />
|
||||
<result property="deviceId" column="device_id" />
|
||||
<result property="deviceSn" column="device_sn" />
|
||||
<result property="authId" column="auth_id" />
|
||||
<result property="beginTime" column="begin_time" />
|
||||
<result property="endTime" column="end_time" />
|
||||
<result property="checkinTime" column="checkin_time" />
|
||||
<result property="checkoutTime" column="checkout_time" />
|
||||
<result property="changeFromId" column="change_from_id"/>
|
||||
<result property="stayStatus" column="stay_status" />
|
||||
<result property="authStatus" column="auth_status" />
|
||||
<result property="authMsg" column="auth_msg" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectStayVo">
|
||||
select s.stay_id, s.stay_no, s.biz_no, s.biz_system, s.source,
|
||||
s.person_id, s.person_no, s.person_name, s.guest_type,
|
||||
s.room_id, s.room_code, r.room_no, s.device_id, s.device_sn, s.auth_id,
|
||||
s.begin_time, s.end_time, s.checkin_time, s.checkout_time, s.change_from_id,
|
||||
s.stay_status, s.auth_status, s.auth_msg, s.del_flag,
|
||||
s.create_by, s.create_time, s.update_by, s.update_time, s.remark
|
||||
from lock_stay s
|
||||
left join lock_room r on r.room_id = s.room_id and r.del_flag = '0'
|
||||
</sql>
|
||||
|
||||
<select id="selectStayList" parameterType="LockStay" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
<where>
|
||||
s.del_flag = '0'
|
||||
<if test="personNo != null and personNo != ''">
|
||||
and s.person_no like concat('%', #{personNo}, '%')
|
||||
</if>
|
||||
<if test="personName != null and personName != ''">
|
||||
and s.person_name like concat('%', #{personName}, '%')
|
||||
</if>
|
||||
<if test="roomId != null">
|
||||
and s.room_id = #{roomId}
|
||||
</if>
|
||||
<if test="roomCode != null and roomCode != ''">
|
||||
and s.room_code like concat('%', #{roomCode}, '%')
|
||||
</if>
|
||||
<if test="deviceId != null">
|
||||
and s.device_id = #{deviceId}
|
||||
</if>
|
||||
<if test="guestType != null and guestType != ''">
|
||||
and s.guest_type = #{guestType}
|
||||
</if>
|
||||
<!-- 老数据没有来源列值时按平台单处理,避免筛选「本平台」漏掉 D20 之前的单据 -->
|
||||
<if test="source != null and source != ''">
|
||||
and coalesce(nullif(s.source, ''), '1') = #{source}
|
||||
</if>
|
||||
<if test="bizSystem != null and bizSystem != ''">
|
||||
and s.biz_system = #{bizSystem}
|
||||
</if>
|
||||
<if test="stayStatus != null and stayStatus != ''">
|
||||
and s.stay_status = #{stayStatus}
|
||||
</if>
|
||||
<if test="authStatus != null and authStatus != ''">
|
||||
and s.auth_status = #{authStatus}
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and date_format(s.begin_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and date_format(s.begin_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
order by s.stay_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectStayById" parameterType="Long" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
where s.stay_id = #{stayId} and s.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectActiveStaysByPersonNo" parameterType="String" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
where s.person_no = #{personNo} and s.stay_status = '1' and s.del_flag = '0'
|
||||
order by s.stay_id desc
|
||||
</select>
|
||||
|
||||
<!-- 退房清理人员前判断是否还有其它未结束的单:待入住(0)与在住(1)都算占用 -->
|
||||
<select id="countActiveStaysByPersonNo" resultType="int">
|
||||
select count(1) from lock_stay
|
||||
where person_no = #{personNo} and del_flag = '0' and stay_status in ('0', '1')
|
||||
<if test="excludeStayId != null">
|
||||
and stay_id != #{excludeStayId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="selectStaysByRoomId" parameterType="Long" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
where s.room_id = #{roomId} and s.del_flag = '0'
|
||||
order by s.stay_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectActiveStaysByRoomId" parameterType="Long" resultMap="LockStayResult">
|
||||
select stay_id, person_no, person_name, room_id, room_code, device_id, device_sn, auth_id,
|
||||
begin_time, end_time, checkin_time, stay_status, auth_status
|
||||
from lock_stay
|
||||
where room_id = #{roomId} and stay_status = '1' and del_flag = '0'
|
||||
order by stay_id desc
|
||||
</select>
|
||||
|
||||
<!--
|
||||
按业务号定位:不过滤 del_flag。唯一键 uk_lock_stay_biz (biz_system, biz_no) 不含 del_flag,
|
||||
已逻辑删除的单仍占用键值;若这里过滤掉,接口重复提交会直接撞唯一键抛 SQL 异常。
|
||||
-->
|
||||
<select id="selectStayByBiz" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
where s.biz_system = #{bizSystem} and s.biz_no = #{bizNo}
|
||||
order by s.stay_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectActiveStayByPersonAndRoom" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
where s.person_no = #{personNo} and s.room_id = #{roomId} and s.stay_status = '1'
|
||||
and s.del_flag = '0'
|
||||
<if test="bizSystem != null and bizSystem != ''">
|
||||
and s.biz_system = #{bizSystem}
|
||||
</if>
|
||||
order by s.stay_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectLatestStayByPersonAndRoom" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
where s.person_no = #{personNo} and s.room_id = #{roomId} and s.del_flag = '0'
|
||||
<if test="bizSystem != null and bizSystem != ''">
|
||||
and s.biz_system = #{bizSystem}
|
||||
</if>
|
||||
order by s.stay_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<!-- 回调用:授权下的入住单。不过滤 del_flag,删单后的回调仍需回填业务单号 -->
|
||||
<select id="selectStayByAuthId" parameterType="Long" resultMap="LockStayResult">
|
||||
<include refid="selectStayVo"/>
|
||||
where s.auth_id = #{authId}
|
||||
order by s.stay_id desc
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="countActiveStaysByDeviceId" parameterType="Long" resultType="int">
|
||||
select count(1) from lock_stay
|
||||
where device_id = #{deviceId} and stay_status = '1' and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countStaysByBizSystem" parameterType="String" resultType="int">
|
||||
select count(1) from lock_stay where biz_system = #{bizSystem}
|
||||
</select>
|
||||
|
||||
<select id="countActiveStays" resultType="int">
|
||||
select count(1) from lock_stay where stay_status = '1' and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<!-- 带上 person_id / biz_system:到期退房后按同一套规则清理外部接口建档人员 -->
|
||||
<select id="selectExpiredStays" resultMap="LockStayResult">
|
||||
select stay_id, biz_system, person_id, person_no, person_name, room_id, room_code, device_id, device_sn,
|
||||
auth_id, begin_time, end_time, checkin_time, stay_status, auth_status, del_flag
|
||||
from lock_stay
|
||||
where stay_status = '1' and del_flag = '0' and end_time <= #{endTime}
|
||||
order by end_time asc
|
||||
limit #{limit}
|
||||
</select>
|
||||
|
||||
<update id="updateExpire" parameterType="Long">
|
||||
update lock_stay
|
||||
set stay_status = '2', checkout_time = end_time, update_time = CURRENT_TIMESTAMP
|
||||
where stay_id = #{stayId} and stay_status = '1'
|
||||
</update>
|
||||
|
||||
<insert id="insertStay" parameterType="LockStay" useGeneratedKeys="true" keyProperty="stayId">
|
||||
insert into lock_stay (
|
||||
stay_no, biz_no, biz_system, source, person_id, person_no, person_name, guest_type,
|
||||
room_id, room_code, device_id, device_sn, auth_id,
|
||||
begin_time, end_time, checkin_time, change_from_id,
|
||||
stay_status, auth_status, auth_msg, del_flag, create_by, create_time, remark
|
||||
) values (
|
||||
#{stayNo}, #{bizNo,jdbcType=VARCHAR}, #{bizSystem,jdbcType=VARCHAR}, #{source},
|
||||
#{personId}, #{personNo}, #{personName}, #{guestType},
|
||||
#{roomId}, #{roomCode}, #{deviceId}, #{deviceSn}, #{authId},
|
||||
#{beginTime}, #{endTime}, #{checkinTime}, #{changeFromId},
|
||||
#{stayStatus}, #{authStatus}, #{authMsg}, '0', #{createBy}, CURRENT_TIMESTAMP, #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateCheckout">
|
||||
update lock_stay
|
||||
set stay_status = '2', checkout_time = #{checkoutTime}, update_time = CURRENT_TIMESTAMP
|
||||
where stay_id = #{stayId} and stay_status = '1'
|
||||
</update>
|
||||
|
||||
<update id="updateChanged">
|
||||
update lock_stay
|
||||
set stay_status = '3', checkout_time = #{checkoutTime}, update_time = CURRENT_TIMESTAMP
|
||||
where stay_id = #{stayId} and stay_status = '1'
|
||||
</update>
|
||||
|
||||
<update id="updateEndTime">
|
||||
update lock_stay
|
||||
set end_time = #{endTime}, update_time = CURRENT_TIMESTAMP
|
||||
where stay_id = #{stayId} and stay_status = '1'
|
||||
</update>
|
||||
|
||||
<update id="updateAuthStatusByAuthId">
|
||||
update lock_stay
|
||||
set auth_status = #{authStatus}, auth_msg = #{authMsg}, update_time = CURRENT_TIMESTAMP
|
||||
where auth_id = #{authId} and del_flag = '0'
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.iot.person.mapper.LockPersonMapper">
|
||||
|
||||
<resultMap type="LockPerson" id="LockPersonResult">
|
||||
<id property="personId" column="person_id" />
|
||||
<result property="personNo" column="person_no" />
|
||||
<result property="personName" column="person_name" />
|
||||
<result property="personType" column="person_type" />
|
||||
<result property="deptId" column="dept_id" />
|
||||
<result property="deptName" column="dept_name" />
|
||||
<result property="sysUserId" column="sys_user_id" />
|
||||
<result property="userName" column="user_name" />
|
||||
<result property="nickName" column="nick_name" />
|
||||
<result property="idCardNo" column="id_card_no" />
|
||||
<result property="phone" column="phone" />
|
||||
<result property="validFrom" column="valid_from" />
|
||||
<result property="validTo" column="valid_to" />
|
||||
<result property="status" column="status" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
<result property="createBy" column="create_by" />
|
||||
<result property="createTime" column="create_time" />
|
||||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectPersonVo">
|
||||
select p.person_id, p.person_no, p.person_name, p.person_type, p.dept_id,
|
||||
d.dept_name, p.sys_user_id, u.user_name, u.nick_name, p.phone,
|
||||
p.valid_from, p.valid_to, p.status, p.del_flag, p.create_by, p.create_time,
|
||||
p.update_by, p.update_time, p.remark
|
||||
from lock_person p
|
||||
left join sys_dept d on d.dept_id = p.dept_id
|
||||
left join sys_user u on u.user_id = p.sys_user_id
|
||||
</sql>
|
||||
|
||||
<!-- 列表不返回证件号,避免敏感信息随导出/翻页扩散 -->
|
||||
<select id="selectPersonList" parameterType="LockPerson" resultMap="LockPersonResult">
|
||||
<include refid="selectPersonVo"/>
|
||||
<where>
|
||||
p.del_flag = '0'
|
||||
<if test="personNo != null and personNo != ''">
|
||||
and p.person_no like concat('%', #{personNo}, '%')
|
||||
</if>
|
||||
<if test="personName != null and personName != ''">
|
||||
and p.person_name like concat('%', #{personName}, '%')
|
||||
</if>
|
||||
<if test="personType != null and personType != ''">
|
||||
and p.person_type = #{personType}
|
||||
</if>
|
||||
<if test="deptId != null">
|
||||
and p.dept_id = #{deptId}
|
||||
</if>
|
||||
<if test="sysUserId != null">
|
||||
and p.sys_user_id = #{sysUserId}
|
||||
</if>
|
||||
<if test="phone != null and phone != ''">
|
||||
and p.phone like concat('%', #{phone}, '%')
|
||||
</if>
|
||||
<if test="status != null and status != ''">
|
||||
and p.status = #{status}
|
||||
</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and date_format(p.create_time,'%Y%m%d') >= date_format(#{params.beginTime},'%Y%m%d')
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and date_format(p.create_time,'%Y%m%d') <= date_format(#{params.endTime},'%Y%m%d')
|
||||
</if>
|
||||
</where>
|
||||
order by p.person_no
|
||||
</select>
|
||||
|
||||
<select id="selectPersonOption" resultMap="LockPersonResult">
|
||||
select p.person_id, p.person_no, p.person_name, p.person_type, p.dept_id,
|
||||
d.dept_name, p.status
|
||||
from lock_person p
|
||||
left join sys_dept d on d.dept_id = p.dept_id
|
||||
where p.del_flag = '0' and p.status = '0'
|
||||
order by p.person_no
|
||||
</select>
|
||||
|
||||
<select id="selectPersonById" parameterType="Long" resultMap="LockPersonResult">
|
||||
select p.person_id, p.person_no, p.person_name, p.person_type, p.dept_id,
|
||||
d.dept_name, p.sys_user_id, u.user_name, u.nick_name, p.id_card_no, p.phone,
|
||||
p.valid_from, p.valid_to, p.status, p.del_flag, p.create_by, p.create_time,
|
||||
p.update_by, p.update_time, p.remark
|
||||
from lock_person p
|
||||
left join sys_dept d on d.dept_id = p.dept_id
|
||||
left join sys_user u on u.user_id = p.sys_user_id
|
||||
where p.person_id = #{personId}
|
||||
</select>
|
||||
|
||||
<select id="selectPersonByNo" parameterType="String" resultMap="LockPersonResult">
|
||||
select p.person_id, p.person_no, p.person_name, p.person_type, p.dept_id,
|
||||
d.dept_name, p.sys_user_id, u.user_name, u.nick_name, p.phone,
|
||||
p.valid_from, p.valid_to, p.status, p.del_flag
|
||||
from lock_person p
|
||||
left join sys_dept d on d.dept_id = p.dept_id
|
||||
left join sys_user u on u.user_id = p.sys_user_id
|
||||
where p.person_no = #{personNo} and p.del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="countPersonNo" resultType="int">
|
||||
select count(1) from lock_person
|
||||
where person_no = #{personNo}
|
||||
<if test="excludeId != null">
|
||||
and person_id != #{excludeId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<insert id="insertPerson" parameterType="LockPerson" useGeneratedKeys="true" keyProperty="personId">
|
||||
insert into lock_person (
|
||||
person_no, person_name, person_type, dept_id, sys_user_id, id_card_no, phone,
|
||||
valid_from, valid_to, status, del_flag, create_by, create_time, remark
|
||||
) values (
|
||||
#{personNo}, #{personName}, #{personType}, #{deptId}, #{sysUserId}, #{idCardNo}, #{phone},
|
||||
#{validFrom}, #{validTo}, #{status}, '0', #{createBy}, CURRENT_TIMESTAMP, #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updatePerson" parameterType="LockPerson">
|
||||
update lock_person
|
||||
<set>
|
||||
<if test="personName != null">person_name = #{personName},</if>
|
||||
<if test="personType != null and personType != ''">person_type = #{personType},</if>
|
||||
<if test="deptId != null">dept_id = #{deptId},</if>
|
||||
<if test="sysUserId != null">sys_user_id = #{sysUserId},</if>
|
||||
<if test="idCardNo != null">id_card_no = #{idCardNo},</if>
|
||||
<if test="phone != null">phone = #{phone},</if>
|
||||
<if test="validFrom != null">valid_from = #{validFrom},</if>
|
||||
<if test="validTo != null">valid_to = #{validTo},</if>
|
||||
<if test="status != null and status != ''">status = #{status},</if>
|
||||
<if test="remark != null">remark = #{remark},</if>
|
||||
update_by = #{updateBy}, update_time = CURRENT_TIMESTAMP
|
||||
</set>
|
||||
where person_id = #{personId}
|
||||
</update>
|
||||
|
||||
<delete id="deletePersonByIds" parameterType="Long">
|
||||
delete from lock_person where person_id in
|
||||
<foreach collection="array" item="personId" open="(" separator="," close=")">
|
||||
#{personId}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
Reference in new issue
Block a user