feat:消费机更新
feat:人员调用钱包部门不调动的bug
This commit is contained in:
1 parent
1c86c227ee
commit
2ca14aa1e0
38 files changed
+1903
-35
No files matched your search
+53
@@ -0,0 +1,53 @@
|
||||
package com.bsd.pjcloud.admin.api.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 终端部门人脸固定上界游标快照查询参数。
|
||||
*
|
||||
* <p>首次请求不传 {@code snapshotMaxId},后续分页必须原样携带首次响应中的上界,
|
||||
* 避免同步期间新增记录改变当前对账集合。</p>
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端部门人脸同步快照查询")
|
||||
public class SysUserFaceSyncSnapshotDto {
|
||||
|
||||
/**
|
||||
* 终端绑定的商户部门主键;查询范围包含该部门及所有未删除子部门。
|
||||
*/
|
||||
@NotNull(message = "商户部门不能为空")
|
||||
@Schema(description = "商户部门主键", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Long merchantDeptId;
|
||||
|
||||
/**
|
||||
* 可选用户主键;MQ 增量通知回查时传入,部门完整快照时为空。
|
||||
*/
|
||||
@Schema(description = "可选用户主键")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 排他起始游标;首次请求为空或 0,后续使用上一页的 nextAfterId。
|
||||
*/
|
||||
@Min(value = 0, message = "人脸同步游标不能小于0")
|
||||
@Schema(description = "排他起始游标")
|
||||
private Long afterId;
|
||||
|
||||
/**
|
||||
* 单页记录数;空值使用服务端默认值,允许范围为 1 至 500。
|
||||
*/
|
||||
@Min(value = 1, message = "人脸同步分页大小不能小于1")
|
||||
@Max(value = 500, message = "人脸同步分页大小不能超过500")
|
||||
@Schema(description = "单页记录数")
|
||||
private Integer limit;
|
||||
|
||||
/**
|
||||
* 首次响应固定的人脸数据库主键上界;首次请求为空,后续分页必须原样携带。
|
||||
*/
|
||||
@Min(value = 0, message = "人脸同步快照上界不能小于0")
|
||||
@Schema(description = "固定快照主键上界")
|
||||
private Long snapshotMaxId;
|
||||
}
|
||||
+31
@@ -12,6 +12,7 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
@@ -73,6 +74,36 @@ public class SysUserFaceEntity extends Model<SysUserFaceEntity> {
|
||||
@Schema(description = "人脸图片")
|
||||
private String faceImg;
|
||||
|
||||
/**
|
||||
* 虹软静态图片质量分,范围为 0 到 1;未检测或检测失败时为空。
|
||||
*/
|
||||
@Schema(description = "虹软静态图片质量分,范围0-1")
|
||||
private BigDecimal qualityScore;
|
||||
|
||||
/**
|
||||
* 终端质量检测状态,持久化协议值为 detected 或 failed;历史未检测记录为空。
|
||||
*/
|
||||
@Schema(description = "终端质量检测状态:detected/failed")
|
||||
private String qualityStatus;
|
||||
|
||||
/**
|
||||
* 终端完成本次静态图片质量检测的本地时间,用于阻止旧结果覆盖新结果。
|
||||
*/
|
||||
@Schema(description = "终端完成质量检测的时间")
|
||||
private LocalDateTime qualityCheckedAt;
|
||||
|
||||
/**
|
||||
* 虹软 SDK 或终端插件质量检测错误码;检测成功时为空。
|
||||
*/
|
||||
@Schema(description = "虹软或终端质量检测错误码")
|
||||
private Integer qualityErrorCode;
|
||||
|
||||
/**
|
||||
* 最后一次成功写入质量结果的终端设备主键,来源于 Hardware 对 DEVICE-ID 的可信解析。
|
||||
*/
|
||||
@Schema(description = "最后一次上报质量的终端ID")
|
||||
private Long qualityDeviceId;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.bsd.pjcloud.admin.api.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 终端人脸同步快照中的单条权威记录。
|
||||
*
|
||||
* <p>记录只包含审核通过且位于目标部门树内的人脸;终端使用 {@code sampleId} 作为虹软搜索标识,
|
||||
* 使用 {@code sourceUpdatedAt} 与图片地址共同判断是否需要重新提取特征。</p>
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端人脸同步快照记录")
|
||||
public class SysUserFaceSyncRecordVo {
|
||||
|
||||
/**
|
||||
* 人脸记录数据库主键,用于游标排序和快照范围约束。
|
||||
*/
|
||||
@Schema(description = "人脸记录数据库主键")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 虹软人脸库搜索标识,来源于人脸记录 {@code auto_code},必须是 32 位正整数且在当前租户内唯一。
|
||||
*/
|
||||
@Schema(description = "虹软人脸库搜索标识")
|
||||
private String sampleId;
|
||||
|
||||
/**
|
||||
* 人脸所属用户主键,MQ 增量通知按该字段回查同一用户的最多三张人脸。
|
||||
*/
|
||||
@Schema(description = "用户主键")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户姓名,仅用于终端本地展示和识别结果元数据。
|
||||
*/
|
||||
@Schema(description = "用户姓名")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户当前所属部门主键,数据来源为 {@code sys_user.dept_id}。
|
||||
*/
|
||||
@Schema(description = "当前所属部门主键")
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户当前所属部门名称,仅用于终端本地展示。
|
||||
*/
|
||||
@Schema(description = "当前所属部门名称")
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 服务端人脸图片地址;地址变化会触发终端重新下载和提取特征。
|
||||
*/
|
||||
@Schema(description = "人脸图片地址")
|
||||
private String faceImg;
|
||||
|
||||
/**
|
||||
* 服务端人脸图片版本时间;未修改的历史记录回退为创建时间,终端将其持久化作为上报版本依据。
|
||||
*/
|
||||
@Schema(description = "服务端人脸图片版本时间")
|
||||
private LocalDateTime sourceUpdatedAt;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.bsd.pjcloud.admin.api.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 终端部门人脸同步的游标快照响应。
|
||||
*
|
||||
* <p>首次请求生成固定的 {@code snapshotMaxId},后续分页必须原样携带;终端只有在全部页面成功返回并校验后,
|
||||
* 才能据此删除不在目标集合中的本地孤儿记录。</p>
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "终端部门人脸同步游标快照")
|
||||
public class SysUserFaceSyncSnapshotVo {
|
||||
|
||||
/**
|
||||
* 当前页面的人脸记录,按数据库主键升序排列。
|
||||
*/
|
||||
@Schema(description = "当前页面记录")
|
||||
private List<SysUserFaceSyncRecordVo> records;
|
||||
|
||||
/**
|
||||
* 下一页起始游标;当前页为空时沿用请求游标,末页无需继续请求。
|
||||
*/
|
||||
@Schema(description = "下一页起始游标")
|
||||
private Long nextAfterId;
|
||||
|
||||
/**
|
||||
* 是否仍有位于固定快照上界内的后续记录。
|
||||
*/
|
||||
@Schema(description = "是否还有下一页")
|
||||
private Boolean hasMore;
|
||||
|
||||
/**
|
||||
* 首次请求时固定的人脸数据库主键上界,后续分页必须原样携带。
|
||||
*/
|
||||
@Schema(description = "固定快照主键上界")
|
||||
private Long snapshotMaxId;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.bsd.pjcloud.admin.api.vo.base;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户部门+商户部门树查询条件接口
|
||||
* 在 {@link BaseIsIncludeTreeInterface} 用户部门包含/不包含的基础上,增加商户部门的包含/不包含
|
||||
*/
|
||||
public interface BaseIsIncludeDeptMchTreeInterface extends BaseIsIncludeTreeInterface {
|
||||
|
||||
/**
|
||||
* 商户部门ids是否包含
|
||||
*/
|
||||
Boolean getIsIncludeMerchantDept();
|
||||
|
||||
void setIsIncludeMerchantDept(Boolean isIncludeMerchantDept);
|
||||
|
||||
/**
|
||||
* 商户部门ids
|
||||
*/
|
||||
List<Long> getMerchantDeptIds();
|
||||
|
||||
void setMerchantDeptIds(List<Long> merchantDeptIds);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.bsd.pjcloud.admin.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 定时任务配置
|
||||
* 启用定时任务功能,支持调岗钱包部门对账等周期性兜底任务
|
||||
*/
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class SchedulingConfig {
|
||||
}
|
||||
+56
@@ -7,9 +7,16 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceSyncSnapshotDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceTerminalDeleteDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceTerminalQualityReportDto;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUserFaceEntity;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceStatusCountVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceEvidenceSourceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceSyncSnapshotVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalDeleteVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalQualityReportVo;
|
||||
import com.bsd.pjcloud.admin.service.SysUserFaceService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
@@ -29,6 +36,7 @@ import org.springframework.http.HttpHeaders;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -69,6 +77,42 @@ public class SysUserFaceController {
|
||||
public R<SysUserFaceEvidenceSourceVo> evidenceSource(@RequestParam Long userId, @RequestParam String sampleId) {
|
||||
return R.ok(sysUserFaceService.evidenceSource(userId, sampleId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询指定用户的人脸状态数量,调用方必须先完成自身业务授权校验。
|
||||
*/
|
||||
@Inner
|
||||
@Operation(summary = "批量查询用户人脸状态数量", description = "返回审核通过和审核中的人脸数量")
|
||||
@PostMapping("/inner/status-count")
|
||||
public R<List<SysUserFaceStatusCountVo>> listStatusCounts(@RequestBody List<Long> userIds) {
|
||||
return R.ok(sysUserFaceService.listStatusCounts(userIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 Hardware 提供的可信部门范围执行终端人脸删除。
|
||||
*
|
||||
* @param dto 商户部门和样本编号
|
||||
* @return 实际删除及已不存在的样本编号
|
||||
*/
|
||||
@Inner
|
||||
@Operation(summary = "终端按部门范围删除人脸")
|
||||
@PostMapping("/inner/terminal-delete")
|
||||
public R<SysUserFaceTerminalDeleteVo> terminalDelete(@Valid @RequestBody SysUserFaceTerminalDeleteDto dto) {
|
||||
return R.ok(sysUserFaceService.terminalDelete(dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 Hardware 提供的可信设备和部门范围保存终端人脸质量结果。
|
||||
*
|
||||
* @param dto 商户部门、来源设备和质量结果
|
||||
* @return 已更新、按版本或状态优先级忽略和已不存在的样本编号
|
||||
*/
|
||||
@Inner
|
||||
@Operation(summary = "终端按部门范围上报人脸质量")
|
||||
@PostMapping("/inner/terminal-quality-report")
|
||||
public R<SysUserFaceTerminalQualityReportVo> terminalReportQuality(@Valid @RequestBody SysUserFaceTerminalQualityReportDto dto) {
|
||||
return R.ok(sysUserFaceService.terminalReportQuality(dto));
|
||||
}
|
||||
/* ************************************* end 远程调用 end ************************************* */
|
||||
|
||||
/* ************************************* start app start ************************************* */
|
||||
@@ -134,6 +178,18 @@ public class SysUserFaceController {
|
||||
dto.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode());
|
||||
return R.ok(sysUserFaceService.allPageJoin(page, dto).getRecords());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按固定数据库主键上界游标返回本部门及子部门的人脸权威快照。
|
||||
*
|
||||
* @param dto 商户部门、可选用户、排他游标、页面大小及固定快照上界
|
||||
* @return 当前页快照数据
|
||||
*/
|
||||
@Operation(summary = "android人脸部门快照", description = "按数据库主键游标同步本部门及子部门审核通过的人脸")
|
||||
@PostMapping("/ad/v3/sync-snapshot")
|
||||
public R<SysUserFaceSyncSnapshotVo> adV3SyncSnapshot(@Valid @RequestBody SysUserFaceSyncSnapshotDto dto) {
|
||||
return R.ok(sysUserFaceService.syncSnapshot(dto));
|
||||
}
|
||||
/* ************************************* start android start ************************************* */
|
||||
|
||||
@Operation(summary = "设置为全局人脸", description = "设置为全局人脸")
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.bsd.pjcloud.admin.handler;
|
||||
|
||||
/**
|
||||
* 用户默认钱包创建事件。
|
||||
*
|
||||
* <p>事件在用户保存事务中发布,只携带异步调用所需的稳定标识,避免跨线程持有已结束的 HTTP 请求对象。</p>
|
||||
*/
|
||||
public final class UserWalletCreateEvent {
|
||||
|
||||
/**
|
||||
* 已保存的用户主键;钱包服务使用该主键回查用户及其自动创建钱包角色。
|
||||
*/
|
||||
private final Long userId;
|
||||
|
||||
/**
|
||||
* 发布事件时的租户主键;为空时沿用下游服务的默认租户处理规则。
|
||||
*/
|
||||
private final Long tenantId;
|
||||
|
||||
/**
|
||||
* 创建用户默认钱包事件。
|
||||
*
|
||||
* @param userId 已保存的用户主键
|
||||
* @param tenantId 用户所属租户主键,非 Web 调用场景下允许为空
|
||||
*/
|
||||
public UserWalletCreateEvent(Long userId, Long tenantId) {
|
||||
this.userId = userId;
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要创建默认钱包的用户主键。
|
||||
*
|
||||
* @return 已保存的用户主键
|
||||
*/
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件发布时的租户主键。
|
||||
*
|
||||
* @return 租户主键,非 Web 调用场景下可能为空
|
||||
*/
|
||||
public Long getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.bsd.pjcloud.admin.handler;
|
||||
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
/**
|
||||
* 用户默认钱包创建事件监听器。
|
||||
*
|
||||
* <p>监听器在用户事务提交成功后才进入 Spring 管理的异步线程池,避免钱包服务回查到未提交的用户数据。
|
||||
* 调用钱包服务失败时进行有限次重试(createWallet、userChange 均为幂等操作),重试仍失败则记录日志,
|
||||
* 由调岗钱包对账定时任务兜底修复,保证钱包部门与用户部门最终一致。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserWalletCreateEventListener {
|
||||
|
||||
/**
|
||||
* 最大尝试次数:首次调用 + 2 次重试。
|
||||
*/
|
||||
private static final int MAX_ATTEMPTS = 3;
|
||||
|
||||
/**
|
||||
* 重试间隔(毫秒)。
|
||||
*/
|
||||
private static final long RETRY_DELAY_MILLIS = 5_000L;
|
||||
|
||||
/**
|
||||
* 钱包服务远程接口,用于创建默认钱包并同步用户部门等关联信息。
|
||||
*/
|
||||
private final RemoteWalletService remoteWalletService;
|
||||
|
||||
/**
|
||||
* 创建用户默认钱包事件监听器。
|
||||
*
|
||||
* @param remoteWalletService 钱包服务远程接口
|
||||
*/
|
||||
public UserWalletCreateEventListener(RemoteWalletService remoteWalletService) {
|
||||
this.remoteWalletService = remoteWalletService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在用户事务提交成功后异步创建默认钱包并通知钱包服务刷新用户信息。
|
||||
*
|
||||
* <p>无事务调用使用 fallbackExecution 直接进入异步线程;此类调用中的用户修改已按单条语句提交,
|
||||
* 不需要等待事务同步回调。异步线程通过 TenantBroker 恢复事件发布时的租户,确保 Feign 请求携带正确租户头。</p>
|
||||
*
|
||||
* @param event 用户主键及事件发布时租户信息
|
||||
*/
|
||||
@Async
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
|
||||
public void createDefaultWallet(UserWalletCreateEvent event) {
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
Boolean success = TenantBroker.applyAs(event.getTenantId(), tenantId -> syncUserWallet(event.getUserId()));
|
||||
if (Boolean.TRUE.equals(success)) {
|
||||
if (attempt > 1) {
|
||||
log.warn("用户钱包同步第{}次尝试成功,userId:{},tenantId:{}", attempt, event.getUserId(), event.getTenantId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
log.error("用户钱包同步第{}/{}次未成功,userId:{},tenantId:{}", attempt, MAX_ATTEMPTS, event.getUserId(), event.getTenantId());
|
||||
} catch (Exception exception) {
|
||||
log.error("用户钱包同步第{}/{}次异常,userId:{},tenantId:{}", attempt, MAX_ATTEMPTS, event.getUserId(), event.getTenantId(), exception);
|
||||
}
|
||||
if (attempt < MAX_ATTEMPTS) {
|
||||
try {
|
||||
Thread.sleep(RETRY_DELAY_MILLIS);
|
||||
} catch (InterruptedException interrupt) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("用户钱包同步重试等待被中断,userId:{},tenantId:{}", event.getUserId(), event.getTenantId());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("用户钱包同步重试耗尽,userId:{},tenantId:{},等待对账任务兜底修复", event.getUserId(), event.getTenantId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建缺失的默认钱包并同步用户部门等关联信息。
|
||||
*
|
||||
* @return 全部调用成功返回 true;任一调用无返回或业务失败返回 false,由外层决定是否重试
|
||||
*/
|
||||
private boolean syncUserWallet(Long userId) {
|
||||
R<?> createResult = remoteWalletService.createWallet(userId);
|
||||
if (createResult == null) {
|
||||
log.error("创建默认钱包失败,userId:{},钱包服务未返回结果", userId);
|
||||
return false;
|
||||
}
|
||||
if (!createResult.isOk()) {
|
||||
log.error("创建默认钱包失败,userId:{},code:{},msg:{}", userId, createResult.getCode(), createResult.getMsg());
|
||||
return false;
|
||||
}
|
||||
|
||||
R<Boolean> changeResult = remoteWalletService.userChange(userId, Boolean.FALSE);
|
||||
if (changeResult == null) {
|
||||
log.error("同步用户钱包信息失败,userId:{},钱包服务未返回结果", userId);
|
||||
return false;
|
||||
}
|
||||
if (!changeResult.isOk()) {
|
||||
log.error("同步用户钱包信息失败,userId:{},code:{},msg:{}", userId, changeResult.getCode(), changeResult.getMsg());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+79
@@ -9,7 +9,9 @@ import com.bsd.pjcloud.admin.api.entity.SysDept;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUserFaceEntity;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceStatusCountVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceEvidenceSourceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceSyncRecordVo;
|
||||
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
|
||||
@@ -51,6 +53,83 @@ public interface SysUserFaceMapper extends PjcloudBaseMapper<SysUserFaceEntity>
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仍存在的目标样本及其用户当前部门,供终端删除执行整批范围校验。
|
||||
*
|
||||
* @param sampleIds 虹软样本编号
|
||||
* @return 人脸记录及用户部门;用户异常缺失时部门为空并由服务层拒绝
|
||||
*/
|
||||
default List<SysUserFaceVo> terminalDeleteCandidates(List<String> sampleIds) {
|
||||
MPJLambdaWrapper<SysUserFaceEntity> wrapper = new MPJLambdaWrapper<SysUserFaceEntity>()
|
||||
.selectAll(SysUserFaceEntity.class)
|
||||
.selectAs(SysUser::getDeptId, SysUserFaceVo::getDeptId)
|
||||
.leftJoin(SysUser.class, SysUser::getUserId, SysUserFaceEntity::getUserId)
|
||||
.in(SysUserFaceEntity::getAutoCode, sampleIds);
|
||||
return selectJoinList(SysUserFaceVo.class, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询仍存在的目标样本、当前质量时间和用户部门,供终端质量上报执行整批范围校验。
|
||||
*
|
||||
* @param sampleIds 虹软样本编号
|
||||
* @return 人脸记录、当前质量字段及用户部门;用户异常缺失时由服务层整批拒绝
|
||||
*/
|
||||
default List<SysUserFaceVo> terminalQualityCandidates(List<String> sampleIds) {
|
||||
MPJLambdaWrapper<SysUserFaceEntity> wrapper = new MPJLambdaWrapper<SysUserFaceEntity>()
|
||||
.selectAll(SysUserFaceEntity.class)
|
||||
.selectAs(SysUser::getDeptId, SysUserFaceVo::getDeptId)
|
||||
.leftJoin(SysUser.class, SysUser::getUserId, SysUserFaceEntity::getUserId)
|
||||
.in(SysUserFaceEntity::getAutoCode, sampleIds);
|
||||
return selectJoinList(SysUserFaceVo.class, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计目标部门树内审核通过且未删除的人脸编号是否存在数值冲突。
|
||||
*
|
||||
* @param deptIds 终端绑定部门及其有效子部门主键
|
||||
* @param processStatus 审核通过状态编码
|
||||
* @return 当前同步范围的重复数字编号分组数量;大于零时禁止下发快照
|
||||
*/
|
||||
Long countSyncDuplicateAutoCodes(@Param("deptIds") List<Long> deptIds, @Param("processStatus") String processStatus);
|
||||
|
||||
/**
|
||||
* 查询目标部门树和固定主键上界内的最大人脸主键,用于建立稳定快照边界。
|
||||
*
|
||||
* @param deptIds 商户部门及其子部门主键
|
||||
* @param userId 可选用户主键,MQ 增量同步时使用
|
||||
* @param processStatus 审核通过状态编码
|
||||
* @return 快照最大主键;没有记录时返回 {@code 0}
|
||||
*/
|
||||
Long selectSyncSnapshotMaxId(@Param("deptIds") List<Long> deptIds, @Param("userId") Long userId, @Param("processStatus") String processStatus);
|
||||
|
||||
/**
|
||||
* 按数据库主键升序读取终端人脸快照的一页,多取一条用于判断是否存在下一页。
|
||||
*
|
||||
* @param deptIds 商户部门及其子部门主键
|
||||
* @param userId 可选用户主键
|
||||
* @param afterId 排他起始游标
|
||||
* @param snapshotMaxId 固定快照主键上界
|
||||
* @param processStatus 审核通过状态编码
|
||||
* @param fetchSize 实际读取数量,通常为页面大小加一
|
||||
* @return 按人脸数据库主键严格升序排列的记录
|
||||
*/
|
||||
List<SysUserFaceSyncRecordVo> listSyncSnapshotRecords(@Param("deptIds") List<Long> deptIds,
|
||||
@Param("userId") Long userId,
|
||||
@Param("afterId") Long afterId,
|
||||
@Param("snapshotMaxId") Long snapshotMaxId,
|
||||
@Param("processStatus") String processStatus,
|
||||
@Param("fetchSize") int fetchSize);
|
||||
|
||||
/**
|
||||
* 按用户批量统计可用与审核中的人脸数量。
|
||||
*
|
||||
* @param userIds 用户ID列表
|
||||
* @param availableStatus 审核通过状态编码
|
||||
* @param pendingStatus 审核中状态编码
|
||||
* @return 各用户的人脸状态数量
|
||||
*/
|
||||
List<SysUserFaceStatusCountVo> listStatusCounts(@Param("userIds") List<Long> userIds, @Param("availableStatus") String availableStatus, @Param("pendingStatus") String pendingStatus);
|
||||
|
||||
default SysUserFaceVo join(Long id) {
|
||||
MPJLambdaWrapper<SysUserFaceEntity> wrapper = new MPJLambdaWrapper<SysUserFaceEntity>()
|
||||
.selectAll(SysUserFaceEntity.class)
|
||||
|
||||
+37
@@ -3,19 +3,56 @@ package com.bsd.pjcloud.admin.service;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceSyncSnapshotDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceTerminalDeleteDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceTerminalQualityReportDto;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUserFaceEntity;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceStatusCountVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceEvidenceSourceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceSyncSnapshotVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalDeleteVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalQualityReportVo;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.flow.task.dto.ProcessNodeRecordAssignUserParamDto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface SysUserFaceService extends IService<SysUserFaceEntity> {
|
||||
|
||||
Long create(SysUserFaceEntity sysUserFace);
|
||||
|
||||
/**
|
||||
* 按固定主键上界游标读取本部门及子部门的审核通过人脸。
|
||||
*
|
||||
* @param dto 商户部门、可选用户、排他游标、页面大小及固定快照上界
|
||||
* @return 当前页快照和下一页信息
|
||||
*/
|
||||
SysUserFaceSyncSnapshotVo syncSnapshot(SysUserFaceSyncSnapshotDto dto);
|
||||
|
||||
/**
|
||||
* 校验全部样本属于目标部门树后执行逻辑删除,任何越权样本都会整批拒绝。
|
||||
*
|
||||
* @param dto 可信商户部门和样本编号
|
||||
* @return 实际删除及已不存在的样本编号
|
||||
*/
|
||||
SysUserFaceTerminalDeleteVo terminalDelete(SysUserFaceTerminalDeleteDto dto);
|
||||
|
||||
/**
|
||||
* 校验全部质量样本属于目标部门树后幂等保存结果,图片版本和成功状态优先级均不得倒退。
|
||||
*
|
||||
* @param dto 可信商户部门、来源设备和质量结果
|
||||
* @return 已更新、按版本或状态优先级忽略和已不存在的样本编号
|
||||
*/
|
||||
SysUserFaceTerminalQualityReportVo terminalReportQuality(SysUserFaceTerminalQualityReportDto dto);
|
||||
|
||||
/**
|
||||
* 批量统计指定用户的人脸可用与审核中数量。
|
||||
*/
|
||||
List<SysUserFaceStatusCountVo> listStatusCounts(List<Long> userIds);
|
||||
|
||||
void edit(SysUserFaceEntity sysUserFace);
|
||||
|
||||
Boolean editGlobalFace(SysUserFaceDto dto);
|
||||
|
||||
+392
@@ -5,15 +5,25 @@ import cn.hutool.core.util.StrUtil;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceSyncSnapshotDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceTerminalDeleteDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceQualityReportItemDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.SysUserFaceTerminalQualityReportDto;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysDept;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUserFaceEntity;
|
||||
import com.bsd.pjcloud.admin.api.utils.DataUtil;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceStatusCountVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceEvidenceSourceVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceSyncRecordVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceSyncSnapshotVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalDeleteVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalQualityReportVo;
|
||||
import com.bsd.pjcloud.admin.api.vo.UserVO;
|
||||
import com.bsd.pjcloud.admin.mapper.SysUserFaceMapper;
|
||||
import com.bsd.pjcloud.admin.service.SysDeptService;
|
||||
@@ -45,10 +55,17 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 人脸样本表
|
||||
@@ -64,6 +81,31 @@ public class SysUserFaceServiceImpl extends ServiceImpl<SysUserFaceMapper, SysUs
|
||||
|
||||
static private final String TAG = "用户人脸";
|
||||
|
||||
/**
|
||||
* 虹软搜索标识允许的最大 32 位正整数。
|
||||
*/
|
||||
private static final long FACE_AUTO_CODE_MAX_VALUE = Integer.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* V3 人脸快照默认页面大小,兼顾终端内存占用和请求次数。
|
||||
*/
|
||||
private static final int FACE_SYNC_DEFAULT_LIMIT = 200;
|
||||
|
||||
/**
|
||||
* V3 人脸快照单页上限,防止异常参数导致数据库和终端瞬时压力过大。
|
||||
*/
|
||||
private static final int FACE_SYNC_MAX_LIMIT = 500;
|
||||
|
||||
/**
|
||||
* 质量检测成功的跨端协议状态值。
|
||||
*/
|
||||
private static final String FACE_QUALITY_STATUS_DETECTED = "detected";
|
||||
|
||||
/**
|
||||
* 质量检测失败的跨端协议状态值。
|
||||
*/
|
||||
private static final String FACE_QUALITY_STATUS_FAILED = "failed";
|
||||
|
||||
private final SysUserService sysUserService;
|
||||
private final SysDeptService sysDeptService;
|
||||
private final SysRoleService sysRoleService;
|
||||
@@ -234,6 +276,356 @@ public class SysUserFaceServiceImpl extends ServiceImpl<SysUserFaceMapper, SysUs
|
||||
return baseMapper.evidenceSource(userId, sampleId, TaskCheckStatusEnum.RESULT.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用一次分组查询返回人脸状态数量,避免调用方按审核状态重复分页并在内存中统计。
|
||||
*/
|
||||
@Override
|
||||
public List<SysUserFaceStatusCountVo> listStatusCounts(List<Long> userIds) {
|
||||
List<Long> distinctUserIds = userIds == null ? List.of() : userIds.stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (distinctUserIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return baseMapper.listStatusCounts(distinctUserIds, TaskCheckStatusEnum.RESULT.getCode(), TaskCheckStatusEnum.PADDING.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按固定数据库主键上界读取本部门及子部门的审核通过人脸快照。
|
||||
*
|
||||
* <p>该方法显式排除外部门全局人脸。调用方必须完整拉取所有页面后再做本地孤儿清理;
|
||||
* 服务端仅审计当前部门树内实际生效的人脸编号;外部门、已删除和未审核历史记录不会阻断本机同步。</p>
|
||||
*
|
||||
* @param dto 商户部门、可选用户、排他游标、页面大小及固定快照上界
|
||||
* @return 当前页面、下一游标和固定快照上界
|
||||
*/
|
||||
@Override
|
||||
public SysUserFaceSyncSnapshotVo syncSnapshot(SysUserFaceSyncSnapshotDto dto) {
|
||||
Long merchantDeptId = dto.getMerchantDeptId();
|
||||
Long userId = dto.getUserId();
|
||||
Long afterId = dto.getAfterId();
|
||||
Integer limit = dto.getLimit();
|
||||
Long snapshotMaxId = dto.getSnapshotMaxId();
|
||||
if (merchantDeptId == null) {
|
||||
throw new RuntimeException("商户部门不能为空");
|
||||
}
|
||||
List<Long> deptIds = sysDeptService.listDescendant(merchantDeptId).stream()
|
||||
.map(SysDept::getDeptId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (deptIds.isEmpty()) {
|
||||
throw new RuntimeException("商户部门不存在或已删除");
|
||||
}
|
||||
assertSyncFaceAutoCodeUnique(deptIds, TaskCheckStatusEnum.RESULT.getCode());
|
||||
|
||||
long normalizedAfterId = afterId == null ? 0L : afterId;
|
||||
if (normalizedAfterId < 0) {
|
||||
throw new RuntimeException("人脸同步游标不能小于0");
|
||||
}
|
||||
int normalizedLimit = limit == null ? FACE_SYNC_DEFAULT_LIMIT : limit;
|
||||
if (normalizedLimit <= 0 || normalizedLimit > FACE_SYNC_MAX_LIMIT) {
|
||||
throw new RuntimeException("人脸同步分页大小必须在1到" + FACE_SYNC_MAX_LIMIT + "之间");
|
||||
}
|
||||
Long normalizedSnapshotMaxId = snapshotMaxId;
|
||||
if (normalizedSnapshotMaxId == null) {
|
||||
normalizedSnapshotMaxId = baseMapper.selectSyncSnapshotMaxId(deptIds, userId, TaskCheckStatusEnum.RESULT.getCode());
|
||||
}
|
||||
if (normalizedSnapshotMaxId == null || normalizedSnapshotMaxId < 0) {
|
||||
throw new RuntimeException("人脸同步快照上界无效");
|
||||
}
|
||||
if (normalizedAfterId > normalizedSnapshotMaxId) {
|
||||
throw new RuntimeException("人脸同步游标超过快照上界");
|
||||
}
|
||||
|
||||
List<SysUserFaceSyncRecordVo> fetchedRecords = baseMapper.listSyncSnapshotRecords(
|
||||
deptIds, userId, normalizedAfterId, normalizedSnapshotMaxId,
|
||||
TaskCheckStatusEnum.RESULT.getCode(), normalizedLimit + 1);
|
||||
boolean hasMore = fetchedRecords.size() > normalizedLimit;
|
||||
List<SysUserFaceSyncRecordVo> records = hasMore
|
||||
? new ArrayList<>(fetchedRecords.subList(0, normalizedLimit))
|
||||
: new ArrayList<>(fetchedRecords);
|
||||
records.forEach(this::validateSyncRecord);
|
||||
|
||||
SysUserFaceSyncSnapshotVo snapshot = new SysUserFaceSyncSnapshotVo();
|
||||
snapshot.setRecords(records);
|
||||
snapshot.setNextAfterId(records.isEmpty() ? normalizedAfterId : records.get(records.size() - 1).getId());
|
||||
snapshot.setHasMore(hasMore);
|
||||
snapshot.setSnapshotMaxId(normalizedSnapshotMaxId);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 Hardware 提供的可信商户部门树批量删除人脸。
|
||||
*
|
||||
* <p>先一次性查询所有仍存在的样本并校验用户当前部门;任意记录不在部门树内时
|
||||
* 立即抛出异常,删除逻辑尚未执行,因此不会产生部分删除。重复样本按首次顺序
|
||||
* 去重;服务端已经不存在的样本作为幂等成功返回给终端。</p>
|
||||
*
|
||||
* @param dto 商户部门和待删除样本编号
|
||||
* @return 实际删除及请求前已不存在的样本编号
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysUserFaceTerminalDeleteVo terminalDelete(SysUserFaceTerminalDeleteDto dto) {
|
||||
if (dto == null || dto.getMerchantDeptId() == null || ObjectUtil.isEmpty(dto.getSampleIds())) {
|
||||
throw new RuntimeException("终端人脸删除参数不能为空");
|
||||
}
|
||||
if (dto.getSampleIds().size() > FACE_SYNC_MAX_LIMIT) {
|
||||
throw new RuntimeException("单次最多删除" + FACE_SYNC_MAX_LIMIT + "条人脸");
|
||||
}
|
||||
|
||||
LinkedHashSet<String> normalizedSet = new LinkedHashSet<>();
|
||||
for (String sampleId : dto.getSampleIds()) {
|
||||
normalizedSet.add(normalizeTerminalSampleId(sampleId));
|
||||
}
|
||||
List<String> sampleIds = new ArrayList<>(normalizedSet);
|
||||
List<Long> deptIds = sysDeptService.listDescendant(dto.getMerchantDeptId()).stream()
|
||||
.map(SysDept::getDeptId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (deptIds.isEmpty()) {
|
||||
throw new RuntimeException("终端绑定部门不存在或已删除");
|
||||
}
|
||||
|
||||
List<SysUserFaceVo> candidates = baseMapper.terminalDeleteCandidates(sampleIds);
|
||||
Set<Long> allowedDeptIds = new HashSet<>(deptIds);
|
||||
boolean outOfScope = candidates.stream()
|
||||
.anyMatch(face -> face.getDeptId() == null || !allowedDeptIds.contains(face.getDeptId()));
|
||||
if (outOfScope) {
|
||||
throw new RuntimeException("请求包含不属于当前设备部门范围的人脸");
|
||||
}
|
||||
|
||||
Set<String> existingSampleIds = candidates.stream()
|
||||
.map(SysUserFaceVo::getAutoCode)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toSet());
|
||||
List<String> deletedSampleIds = sampleIds.stream()
|
||||
.filter(existingSampleIds::contains)
|
||||
.toList();
|
||||
List<String> alreadyAbsentSampleIds = sampleIds.stream()
|
||||
.filter(sampleId -> !existingSampleIds.contains(sampleId))
|
||||
.toList();
|
||||
ArrayList<Long> databaseIds = candidates.stream()
|
||||
.map(SysUserFaceVo::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
delByIds(databaseIds);
|
||||
return SysUserFaceTerminalDeleteVo.builder()
|
||||
.deletedSampleIds(deletedSampleIds)
|
||||
.alreadyAbsentSampleIds(alreadyAbsentSampleIds)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 Hardware 提供的可信设备和商户部门树幂等保存人脸质量结果。
|
||||
*
|
||||
* <p>先校验完整请求、审核通过状态和部门范围,再逐条按图片版本与状态优先级更新。
|
||||
* 质量回写故意不修改 {@code update_time} 且不发布人脸变更事件,避免终端将
|
||||
* 质量变化误判为图片版本变化并形成重复下载、检测和上报循环。</p>
|
||||
*
|
||||
* @param dto 商户部门、来源设备和质量结果
|
||||
* @return 已更新、按版本或状态优先级忽略和已不存在的样本编号
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SysUserFaceTerminalQualityReportVo terminalReportQuality(SysUserFaceTerminalQualityReportDto dto) {
|
||||
if (dto == null || dto.getMerchantDeptId() == null || dto.getQualityDeviceId() == null
|
||||
|| dto.getQualityDeviceId() <= 0 || ObjectUtil.isEmpty(dto.getItems())) {
|
||||
throw new RuntimeException("终端人脸质量上报参数不能为空");
|
||||
}
|
||||
if (dto.getItems().size() > FACE_SYNC_MAX_LIMIT) {
|
||||
throw new RuntimeException("单次最多上报" + FACE_SYNC_MAX_LIMIT + "条人脸质量结果");
|
||||
}
|
||||
|
||||
LinkedHashMap<String, SysUserFaceQualityReportItemDto> itemBySampleId = new LinkedHashMap<>();
|
||||
for (SysUserFaceQualityReportItemDto item : dto.getItems()) {
|
||||
validateQualityReportItem(item);
|
||||
String sampleId = normalizeTerminalSampleId(item.getSampleId());
|
||||
item.setSampleId(sampleId);
|
||||
if (itemBySampleId.putIfAbsent(sampleId, item) != null) {
|
||||
throw new RuntimeException("人脸质量上报包含重复样本编号");
|
||||
}
|
||||
}
|
||||
|
||||
List<Long> deptIds = sysDeptService.listDescendant(dto.getMerchantDeptId()).stream()
|
||||
.map(SysDept::getDeptId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (deptIds.isEmpty()) {
|
||||
throw new RuntimeException("终端绑定部门不存在或已删除");
|
||||
}
|
||||
|
||||
List<SysUserFaceVo> candidates = baseMapper.terminalQualityCandidates(
|
||||
new ArrayList<>(itemBySampleId.keySet()));
|
||||
Set<Long> allowedDeptIds = new HashSet<>(deptIds);
|
||||
boolean outOfScope = candidates.stream()
|
||||
.anyMatch(face -> face.getDeptId() == null || !allowedDeptIds.contains(face.getDeptId()));
|
||||
if (outOfScope) {
|
||||
throw new RuntimeException("请求包含不属于当前设备部门范围的人脸");
|
||||
}
|
||||
boolean notApproved = candidates.stream()
|
||||
.anyMatch(face -> !TaskCheckStatusEnum.RESULT.getCode().equals(face.getProcessStatus()));
|
||||
if (notApproved) {
|
||||
throw new RuntimeException("请求包含未审核通过的人脸");
|
||||
}
|
||||
|
||||
LinkedHashMap<String, SysUserFaceVo> candidateBySampleId = new LinkedHashMap<>();
|
||||
for (SysUserFaceVo candidate : candidates) {
|
||||
String sampleId = StrUtil.trim(candidate.getAutoCode());
|
||||
if (candidate.getId() == null || StrUtil.isBlank(sampleId)
|
||||
|| candidateBySampleId.putIfAbsent(sampleId, candidate) != null) {
|
||||
throw new RuntimeException("服务端存在重复或无效人脸样本编号");
|
||||
}
|
||||
}
|
||||
|
||||
List<String> updatedSampleIds = new ArrayList<>();
|
||||
List<String> ignoredStaleSampleIds = new ArrayList<>();
|
||||
List<String> alreadyAbsentSampleIds = new ArrayList<>();
|
||||
for (Map.Entry<String, SysUserFaceQualityReportItemDto> entry : itemBySampleId.entrySet()) {
|
||||
String sampleId = entry.getKey();
|
||||
SysUserFaceQualityReportItemDto item = entry.getValue();
|
||||
SysUserFaceVo candidate = candidateBySampleId.get(sampleId);
|
||||
if (candidate == null) {
|
||||
alreadyAbsentSampleIds.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
LocalDateTime sourceUpdatedAt = candidate.getUpdateTime() == null
|
||||
? candidate.getCreateTime()
|
||||
: candidate.getUpdateTime();
|
||||
if (sourceUpdatedAt == null || !sourceUpdatedAt.equals(item.getSourceUpdatedAt())) {
|
||||
ignoredStaleSampleIds.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
// 同一图片版本的首个有效 detected 是服务端权威结果,失败设备和重复设备均不得覆盖。
|
||||
if (FACE_QUALITY_STATUS_DETECTED.equals(candidate.getQualityStatus())) {
|
||||
ignoredStaleSampleIds.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
if (FACE_QUALITY_STATUS_FAILED.equals(item.getStatus())
|
||||
&& candidate.getQualityCheckedAt() != null
|
||||
&& !item.getCheckedAt().isAfter(candidate.getQualityCheckedAt())) {
|
||||
ignoredStaleSampleIds.add(sampleId);
|
||||
continue;
|
||||
}
|
||||
|
||||
LambdaUpdateWrapper<SysUserFaceEntity> updateWrapper = new LambdaUpdateWrapper<SysUserFaceEntity>()
|
||||
.set(SysUserFaceEntity::getQualityScore, item.getScore())
|
||||
.set(SysUserFaceEntity::getQualityStatus, item.getStatus())
|
||||
.set(SysUserFaceEntity::getQualityCheckedAt, item.getCheckedAt())
|
||||
.set(SysUserFaceEntity::getQualityErrorCode, item.getErrorCode())
|
||||
.set(SysUserFaceEntity::getQualityDeviceId, dto.getQualityDeviceId())
|
||||
.eq(SysUserFaceEntity::getId, candidate.getId())
|
||||
.and(version -> version.eq(SysUserFaceEntity::getUpdateTime, item.getSourceUpdatedAt())
|
||||
.or(fallback -> fallback.isNull(SysUserFaceEntity::getUpdateTime)
|
||||
.eq(SysUserFaceEntity::getCreateTime, item.getSourceUpdatedAt())));
|
||||
if (FACE_QUALITY_STATUS_DETECTED.equals(item.getStatus())) {
|
||||
updateWrapper.and(status -> status.isNull(SysUserFaceEntity::getQualityStatus)
|
||||
.or()
|
||||
.ne(SysUserFaceEntity::getQualityStatus, FACE_QUALITY_STATUS_DETECTED));
|
||||
} else {
|
||||
updateWrapper.and(status -> status.isNull(SysUserFaceEntity::getQualityStatus)
|
||||
.or(failed -> failed.eq(SysUserFaceEntity::getQualityStatus, FACE_QUALITY_STATUS_FAILED)
|
||||
.and(time -> time.isNull(SysUserFaceEntity::getQualityCheckedAt)
|
||||
.or()
|
||||
.lt(SysUserFaceEntity::getQualityCheckedAt, item.getCheckedAt()))));
|
||||
}
|
||||
int updatedRows = baseMapper.update(null, updateWrapper);
|
||||
if (updatedRows > 0) {
|
||||
updatedSampleIds.add(sampleId);
|
||||
} else {
|
||||
ignoredStaleSampleIds.add(sampleId);
|
||||
}
|
||||
}
|
||||
return SysUserFaceTerminalQualityReportVo.builder()
|
||||
.updatedSampleIds(updatedSampleIds)
|
||||
.ignoredStaleSampleIds(ignoredStaleSampleIds)
|
||||
.alreadyAbsentSampleIds(alreadyAbsentSampleIds)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验一条质量结果的状态、分数、错误码和检测时间组合。
|
||||
*
|
||||
* @param item 待校验的终端质量结果
|
||||
*/
|
||||
private void validateQualityReportItem(SysUserFaceQualityReportItemDto item) {
|
||||
if (item == null || item.getSourceUpdatedAt() == null || item.getCheckedAt() == null) {
|
||||
throw new RuntimeException("人脸质量结果、图片版本或检测时间不能为空");
|
||||
}
|
||||
String status = StrUtil.trim(item.getStatus());
|
||||
item.setStatus(status);
|
||||
if (FACE_QUALITY_STATUS_DETECTED.equals(status)) {
|
||||
if (item.getScore() == null || item.getScore().compareTo(BigDecimal.ZERO) < 0
|
||||
|| item.getScore().compareTo(BigDecimal.ONE) > 0) {
|
||||
throw new RuntimeException("检测成功的人脸质量分必须在0到1之间");
|
||||
}
|
||||
if (item.getErrorCode() != null) {
|
||||
throw new RuntimeException("检测成功的人脸质量结果不能包含错误码");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (FACE_QUALITY_STATUS_FAILED.equals(status)) {
|
||||
if (item.getScore() != null) {
|
||||
throw new RuntimeException("检测失败的人脸质量结果不能包含分数");
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new RuntimeException("人脸质量检测状态仅支持detected或failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* 将终端样本编号规范化为虹软可接受的 32 位正整数文本。
|
||||
*
|
||||
* @param sampleId 原始样本编号
|
||||
* @return 去除首尾空白后的样本编号
|
||||
*/
|
||||
private String normalizeTerminalSampleId(String sampleId) {
|
||||
String normalized = StrUtil.trim(sampleId);
|
||||
try {
|
||||
long numeric = Long.parseLong(normalized);
|
||||
if (numeric <= 0 || numeric > FACE_AUTO_CODE_MAX_VALUE) {
|
||||
throw new NumberFormatException("out of range");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException("存在无效人脸样本编号");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验快照记录的虹软搜索标识可被 Android 端安全解析为 32 位正整数。
|
||||
*
|
||||
* @param record 待下发的快照记录
|
||||
*/
|
||||
private void validateSyncRecord(SysUserFaceSyncRecordVo record) {
|
||||
try {
|
||||
long sampleId = Long.parseLong(record.getSampleId());
|
||||
if (sampleId <= 0 || sampleId > FACE_AUTO_CODE_MAX_VALUE) {
|
||||
throw new NumberFormatException("out of range");
|
||||
}
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new RuntimeException("存在无效的人脸编号,faceId=" + record.getId() + ",请先完成数据修复", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 审计当前终端部门树内实际生效的人脸编号是否冲突。
|
||||
*
|
||||
* <p>同步只维护本部门及子部门,因此外部门、已删除和未审核记录即使存在历史重复编号,
|
||||
* 也不应阻断当前终端;当前范围内的冲突仍必须停止下发,避免虹软相同搜索标识互相覆盖。</p>
|
||||
*
|
||||
* @param deptIds 终端绑定部门及其有效子部门主键
|
||||
* @param processStatus 审核通过状态编码
|
||||
*/
|
||||
private void assertSyncFaceAutoCodeUnique(List<Long> deptIds, String processStatus) {
|
||||
Long duplicateCount = baseMapper.countSyncDuplicateAutoCodes(deptIds, processStatus);
|
||||
if (duplicateCount != null && duplicateCount > 0) {
|
||||
throw new RuntimeException("当前部门范围存在重复的人脸编号,已停止同步,请先修复冲突数据");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启审批
|
||||
*/
|
||||
|
||||
+5
-9
@@ -45,6 +45,7 @@ import com.bsd.pjcloud.admin.api.utils.DataUtil;
|
||||
import com.bsd.pjcloud.admin.api.vo.*;
|
||||
import com.bsd.pjcloud.admin.mapper.SysUserFaceMapper;
|
||||
import com.bsd.pjcloud.admin.mapper.SysUserMapper;
|
||||
import com.bsd.pjcloud.admin.handler.UserWalletCreateEvent;
|
||||
import com.bsd.pjcloud.admin.mapper.SysUserPostMapper;
|
||||
import com.bsd.pjcloud.admin.mapper.SysUserRoleMapper;
|
||||
import com.bsd.pjcloud.admin.service.*;
|
||||
@@ -80,6 +81,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.redis.core.Cursor;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
@@ -136,6 +138,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
private final RemoteWalletService remoteWalletService;
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
private final SysFileService sysFileService;
|
||||
|
||||
@@ -208,15 +211,8 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
if (isDeptAdmin) {
|
||||
getSelf().applyFlow(sysUser);
|
||||
} else {
|
||||
// 创建用户默认钱包、保证线程header
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
RequestContextHolder.setRequestAttributes(requestAttributes);
|
||||
// 创建钱包
|
||||
remoteWalletService.createWallet(sysUser.getUserId());
|
||||
// 通知钱包
|
||||
remoteWalletService.userChange(sysUser.getUserId(), Boolean.FALSE);
|
||||
});
|
||||
// 事务提交前其他连接无法读取新用户,因此仅发布事件,由监听器在提交成功后异步创建钱包。
|
||||
applicationEventPublisher.publishEvent(new UserWalletCreateEvent(sysUser.getUserId(), TenantContextHolder.getTenantId()));
|
||||
}
|
||||
|
||||
return sysUser.getUserId();
|
||||
|
||||
+5
-11
@@ -18,6 +18,7 @@ import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUserTransactionEntity;
|
||||
import com.bsd.pjcloud.admin.api.vo.SysUserTransactionVo;
|
||||
import com.bsd.pjcloud.admin.mapper.SysUserTransactionMapper;
|
||||
import com.bsd.pjcloud.admin.handler.UserWalletCreateEvent;
|
||||
import com.bsd.pjcloud.admin.service.SysDeptService;
|
||||
import com.bsd.pjcloud.admin.service.SysUserService;
|
||||
import com.bsd.pjcloud.admin.service.SysUserTransactionService;
|
||||
@@ -34,22 +35,19 @@ import com.bsd.pjcloud.flow.task.constant.TaskCheckStatusEnum;
|
||||
import com.bsd.pjcloud.flow.task.dto.ProcessNodeRecordAssignUserParamDto;
|
||||
import com.bsd.pjcloud.flow.task.dto.TaskParamDto;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletRuleTransactionService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletThirdZyService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 人员调动表
|
||||
@@ -69,7 +67,7 @@ public class SysUserTransactionServiceImpl extends ServiceImpl<SysUserTransactio
|
||||
|
||||
private final RemoteFlowEngineService remoteFlowEngineService;
|
||||
private final RemoteWalletThirdZyService remoteWalletThirdZyService;
|
||||
private final RemoteWalletService remoteWalletService;
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
private final RemoteWalletRuleTransactionService walletRuleTransactionService;
|
||||
|
||||
private final FlowUtil flowUtil;
|
||||
@@ -222,12 +220,8 @@ public class SysUserTransactionServiceImpl extends ServiceImpl<SysUserTransactio
|
||||
// 通知正元
|
||||
remoteWalletThirdZyService.syncUserById(detail.getUserId());
|
||||
}
|
||||
// 通知钱包进行相关修改
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
RequestContextHolder.setRequestAttributes(requestAttributes);
|
||||
remoteWalletService.userChange(detail.getUserId(), Boolean.FALSE);
|
||||
});
|
||||
// 通知钱包进行相关修改:发布事件由监听器异步执行,携带租户上下文并记录失败日志
|
||||
applicationEventPublisher.publishEvent(new UserWalletCreateEvent(detail.getUserId(), TenantContextHolder.getTenantId()));
|
||||
sysUserService.deleteDiscountsCacheByUserId(detail.getUserId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.bsd.pjcloud.admin.task;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUserTransactionEntity;
|
||||
import com.bsd.pjcloud.admin.handler.UserWalletCreateEvent;
|
||||
import com.bsd.pjcloud.admin.service.SysUserTransactionService;
|
||||
import com.bsd.pjcloud.admin.service.SysUserService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantBroker;
|
||||
import com.bsd.pjcloud.flow.task.constant.TaskCheckStatusEnum;
|
||||
import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum;
|
||||
import com.bsd.pjcloud.wallet.api.entity.WalletManageEntity;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 调岗审批后钱包部门对账任务。
|
||||
*
|
||||
* <p>审批回调通过事件异步通知钱包服务并带重试,但钱包服务长时间不可用等情况下重试仍会耗尽,
|
||||
* 导致钱包部门与用户部门不一致。本任务周期性比对近期审批通过的调岗记录,发现不一致即重新发布同步事件,
|
||||
* 使钱包部门与用户部门最终一致。</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class SysUserTransactionWalletSyncTask {
|
||||
|
||||
private static final String TAG = "调岗钱包对账";
|
||||
|
||||
/**
|
||||
* 对账窗口:比对最近 N 天内审批通过的调岗记录。
|
||||
*/
|
||||
private static final int CHECK_DAYS = 7;
|
||||
|
||||
private final SysUserTransactionService sysUserTransactionService;
|
||||
private final SysUserService sysUserService;
|
||||
private final RemoteWalletService remoteWalletService;
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
/**
|
||||
* 每小时对账一次,兜底审批回调时丢失的钱包部门同步。
|
||||
*
|
||||
* <p>定时线程无租户上下文,租户拦截器对空租户不过滤,因此初始查询能覆盖全部租户的记录;
|
||||
* 单条记录的比对在记录自身租户下执行,保证用户和钱包查询的租户条件正确。</p>
|
||||
*/
|
||||
@Scheduled(cron = "0 25 * * * ?")
|
||||
public void reconcileWalletDept() {
|
||||
try {
|
||||
List<SysUserTransactionEntity> list = sysUserTransactionService.list(Wrappers.<SysUserTransactionEntity>lambdaQuery()
|
||||
.eq(SysUserTransactionEntity::getProcessStatus, TaskCheckStatusEnum.RESULT.getCode())
|
||||
.ge(SysUserTransactionEntity::getProcessTime, LocalDateTime.now().minusDays(CHECK_DAYS)));
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
// 同一用户多条调岗记录只对账一次,以最新记录的租户为准
|
||||
Map<Long, Long> userTenantMap = list.stream().collect(Collectors.toMap(
|
||||
SysUserTransactionEntity::getUserId, SysUserTransactionEntity::getTenantId, (first, second) -> second));
|
||||
int mismatchCount = 0;
|
||||
for (Map.Entry<Long, Long> entry : userTenantMap.entrySet()) {
|
||||
Long userId = entry.getKey();
|
||||
try {
|
||||
if (isWalletDeptMismatched(userId, entry.getValue())) {
|
||||
mismatchCount++;
|
||||
log.warn("|{}|用户<{}>钱包部门与用户部门不一致,重新触发钱包同步,tenantId:{}", TAG, userId, entry.getValue());
|
||||
applicationEventPublisher.publishEvent(new UserWalletCreateEvent(userId, entry.getValue()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("|{}|用户<{}>对账异常", TAG, userId, e);
|
||||
}
|
||||
}
|
||||
if (mismatchCount > 0) {
|
||||
log.warn("|{}|比对{}个用户,{}个钱包部门不一致已重新触发同步", TAG, userTenantMap.size(), mismatchCount);
|
||||
} else {
|
||||
log.info("|{}|比对{}个用户,钱包部门全部一致", TAG, userTenantMap.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("|{}|执行异常", TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户饭卡钱包部门是否与用户当前部门不一致。
|
||||
*
|
||||
* @return 不一致返回 true;用户已删除、部门为空、没有饭卡钱包等无法比对的情况返回 false
|
||||
*/
|
||||
private boolean isWalletDeptMismatched(Long userId, Long tenantId) {
|
||||
return Boolean.TRUE.equals(TenantBroker.applyAs(tenantId, tenant -> {
|
||||
SysUser user = sysUserService.getById(userId);
|
||||
if (user == null || user.getDeptId() == null) {
|
||||
return false;
|
||||
}
|
||||
R<WalletManageEntity> walletResult = remoteWalletService.getWallet(userId, WalletTypeEnum.EAT);
|
||||
if (walletResult == null || !walletResult.isOk() || walletResult.getData() == null) {
|
||||
return false;
|
||||
}
|
||||
return !ObjectUtil.equals(walletResult.getData().getDeptId(), user.getDeptId());
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -126,4 +126,79 @@
|
||||
order by a.create_time desc, a.id desc
|
||||
</select>
|
||||
|
||||
|
||||
<select id="countSyncDuplicateAutoCodes" resultType="java.lang.Long">
|
||||
select count(0)
|
||||
from (
|
||||
select cast(a.auto_code as unsigned) as numeric_auto_code
|
||||
from sys_user_face a
|
||||
inner join sys_user u on u.user_id = a.user_id and u.del_flag = '0'
|
||||
inner join sys_dept d on d.dept_id = u.dept_id and d.del_flag = '0'
|
||||
where a.del_flag = '0'
|
||||
and a.process_status = #{processStatus}
|
||||
and a.auto_code regexp '^[0-9]+$'
|
||||
and d.dept_id in
|
||||
<foreach collection="deptIds" item="deptId" open="(" close=")" separator=",">
|
||||
#{deptId}
|
||||
</foreach>
|
||||
group by cast(a.auto_code as unsigned)
|
||||
having count(0) > 1
|
||||
) duplicate_codes
|
||||
</select>
|
||||
|
||||
<select id="selectSyncSnapshotMaxId" resultType="java.lang.Long">
|
||||
select coalesce(max(a.id), 0)
|
||||
from sys_user_face a
|
||||
inner join sys_user u on u.user_id = a.user_id and u.del_flag = '0'
|
||||
where a.del_flag = '0'
|
||||
and a.process_status = #{processStatus}
|
||||
and u.dept_id in
|
||||
<foreach collection="deptIds" item="deptId" open="(" close=")" separator=",">
|
||||
#{deptId}
|
||||
</foreach>
|
||||
<if test="userId != null">
|
||||
and a.user_id = #{userId}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="listSyncSnapshotRecords" resultType="com.bsd.pjcloud.admin.api.vo.SysUserFaceSyncRecordVo">
|
||||
select a.id,
|
||||
a.auto_code as sampleId,
|
||||
a.user_id as userId,
|
||||
u.name as username,
|
||||
d.dept_id as deptId,
|
||||
d.name as deptName,
|
||||
a.face_img as faceImg,
|
||||
coalesce(a.update_time, a.create_time) as sourceUpdatedAt
|
||||
from sys_user_face a
|
||||
inner join sys_user u on u.user_id = a.user_id and u.del_flag = '0'
|
||||
inner join sys_dept d on d.dept_id = u.dept_id and d.del_flag = '0'
|
||||
where a.del_flag = '0'
|
||||
and a.process_status = #{processStatus}
|
||||
and a.id > #{afterId}
|
||||
and a.id <= #{snapshotMaxId}
|
||||
and d.dept_id in
|
||||
<foreach collection="deptIds" item="deptId" open="(" close=")" separator=",">
|
||||
#{deptId}
|
||||
</foreach>
|
||||
<if test="userId != null">
|
||||
and a.user_id = #{userId}
|
||||
</if>
|
||||
order by a.id asc
|
||||
limit #{fetchSize}
|
||||
</select>
|
||||
|
||||
<select id="listStatusCounts" resultType="com.bsd.pjcloud.admin.api.vo.SysUserFaceStatusCountVo">
|
||||
select user_id as userId,
|
||||
sum(case when process_status = #{availableStatus} then 1 else 0 end) as availableCount,
|
||||
sum(case when process_status = #{pendingStatus} then 1 else 0 end) as pendingCount
|
||||
from sys_user_face
|
||||
where del_flag = '0'
|
||||
and user_id in
|
||||
<foreach collection="userIds" item="userId" open="(" separator="," close=")">
|
||||
#{userId}
|
||||
</foreach>
|
||||
and process_status in (#{availableStatus}, #{pendingStatus})
|
||||
group by user_id
|
||||
</select>
|
||||
</mapper>
|
||||
Reference in new issue
Block a user