diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysUserDeleteDto.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysUserDeleteDto.java
new file mode 100644
index 000000000..3db286107
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysUserDeleteDto.java
@@ -0,0 +1,27 @@
+package com.bsd.pjcloud.admin.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 删除用户请求项。
+ *
流程信息来自用户列表快照,后端删除时仍以数据库中的实时审批状态和流程ID为准。
+ */
+@Data
+@Schema(description = "删除用户请求项")
+public class SysUserDeleteDto {
+
+ /**
+ * 待删除的系统用户ID。
+ */
+ @NotNull(message = "用户ID不能为空")
+ @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Long userId;
+
+ /**
+ * 前端列表中的审批流程实例ID快照;允许为空,不作为后端取消流程的可信数据源。
+ */
+ @Schema(description = "审批流程实例ID快照")
+ private String processId;
+}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
index 1f63663ee..db6671885 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
@@ -27,6 +27,7 @@ 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.ForgotPasswordDTO;
+import com.bsd.pjcloud.admin.api.dto.SysUserDeleteDto;
import com.bsd.pjcloud.admin.api.dto.UserDTO;
import com.bsd.pjcloud.admin.api.dto.UserDeptSimpleInfo;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
@@ -45,6 +46,7 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.excel.annotation.RequestExcel;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
+import com.bsd.pjcloud.common.idempotent.annotation.Idempotent;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.annotation.HasPermission;
import com.bsd.pjcloud.common.security.annotation.Inner;
@@ -247,17 +249,24 @@ public class SysUserController {
}
/**
- * 删除用户信息
+ * 删除用户信息;审核中的用户会先取消当前审批流程。
*
- * @param ids ID
- * @return R
+ * @param users 待删除用户及其前端流程快照
+ * @return 删除结果;请求为空或删除前置校验失败时返回失败信息
*/
@SysLog("删除用户信息")
@DeleteMapping
@HasPermission("sys_user_del")
- @Operation(summary = "删除用户", description = "根据ID删除用户")
- public R userDel(@RequestBody Long[] ids) {
- return R.ok(userService.deleteUserByIds(ids));
+ @Operation(summary = "删除用户", description = "根据ID删除用户,审核中的用户会先取消审批流程")
+ public R userDel(@Valid @RequestBody List users) {
+ if (CollUtil.isEmpty(users)) {
+ return R.failed("请选择要删除的用户");
+ }
+ if (users.stream().anyMatch(user -> user == null || user.getUserId() == null)) {
+ return R.failed("用户ID不能为空");
+ }
+ Long[] userIds = users.stream().map(SysUserDeleteDto::getUserId).distinct().toArray(Long[]::new);
+ return R.ok(userService.deleteUserByIds(userIds));
}
/**
@@ -411,16 +420,30 @@ public class SysUserController {
}
/**
- * 导入学生用户
+ * 导入学生用户。
+ *
+ * @param studentUserExcelList Excel 中解析出的学生用户数据
+ * @param bindingResult Excel 通用校验产生的错误信息
+ * @return 导入结果;失败时包含对应行号和错误原因
*/
@PostMapping("/import-student")
@HasPermission("sys_student_user_export")
+ @Idempotent(expireTime = 5, key = "'/user/import-student:' + #studentUserExcelList.hashCode() + ':' + T(com.bsd.pjcloud.common.security.util.SecurityUtils).getUser().getId()", info = "正在导入中,请勿重复操作")
public R importStudentUser(@RequestExcel List studentUserExcelList, BindingResult bindingResult) {
return userService.importStudentUser(studentUserExcelList, bindingResult);
}
+ /**
+ * 导入用户及 Excel 中内嵌的人脸图片。
+ *
+ * @param file 原始 Excel 文件,用于读取内嵌的人脸图片
+ * @param excelList Excel 中解析出的用户数据
+ * @param bindingResult Excel 通用校验产生的错误信息
+ * @return 导入结果;失败时包含校验错误或人脸图片处理错误
+ */
@PostMapping("/import-face")
@HasPermission("sys_user_export")
+ @Idempotent(expireTime = 5, key = "'/user/import-face:' + #file.getSize() + ':' + #excelList.hashCode() + ':' + T(com.bsd.pjcloud.common.security.util.SecurityUtils).getUser().getId()", info = "正在导入中,请勿重复操作")
public R importUserFace(@RequestParam("file") MultipartFile file, @RequestExcel List excelList, BindingResult bindingResult) {
return userService.importUserFace(file, excelList, bindingResult);
}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
index 6e2394041..36da87b2e 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
@@ -64,10 +64,10 @@ public interface SysUserService extends IService {
IPage innerPage(Page page, UserDTO userDTO);
/**
- * 删除用户
+ * 删除指定用户,并在删除前取消其中仍处于审核中的审批流程。
*
- * @param ids 用户
- * @return boolean
+ * @param ids 待删除的用户ID数组,不能为空
+ * @return 用户及其关联数据进入删除流程后返回 {@code true}
*/
Boolean deleteUserByIds(Long[] ids);
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
index bcee9f2dc..f43cc0af2 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
@@ -79,9 +79,11 @@ import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
import com.bsd.pjcloud.common.excel.vo.ErrorMessage;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
+import com.bsd.pjcloud.flow.task.api.feign.RemoteFlowEngineService;
import com.bsd.pjcloud.flow.task.constant.TaskCheckStatusEnum;
import com.bsd.pjcloud.flow.task.dto.ProcessNodeRecordAssignUserParamDto;
import com.bsd.pjcloud.flow.task.dto.TaskDto;
+import com.bsd.pjcloud.flow.task.dto.TaskParamDto;
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
import cn.hutool.extra.spring.SpringUtil;
@@ -172,6 +174,11 @@ public class SysUserServiceImpl extends ServiceImpl impl
private final RemoteWalletService remoteWalletService;
+ /**
+ * 审批流程远程服务,用于在删除审核中用户前终止对应流程实例。
+ */
+ private final RemoteFlowEngineService remoteFlowEngineService;
+
private final SysFileService sysFileService;
private final FlowUtil flowUtil;
@@ -403,16 +410,23 @@ public class SysUserServiceImpl extends ServiceImpl impl
}
/**
- * 删除用户
+ * 删除用户,并在删除前终止仍处于审核中的用户审批流程。
+ * 审批状态和流程ID以数据库实时数据为准;流程取消失败时抛出异常并停止本次删除。
*
* @param ids 用户ID 列表
- * @return Boolean
+ * @return 所有用户及其关联数据进入删除流程后返回 {@code true}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteUserByIds(Long[] ids) {
+ if (ArrayUtil.isEmpty(ids)) {
+ throw new RuntimeException("请选择要删除的用户");
+ }
List userIds = CollUtil.toList(ids);
List userList = baseMapper.selectBatchIds(userIds);
+ if (userList.size() != userIds.stream().distinct().count()) {
+ throw new RuntimeException("部分用户不存在或已删除,请刷新后重试");
+ }
// 判断是否可以删除
userList.forEach(v -> {
String errorMsg = remoteWalletService.isDel(v.getUserId()).getData();
@@ -421,6 +435,27 @@ public class SysUserServiceImpl extends ServiceImpl impl
}
});
+ // 只终止数据库中仍为审核中的流程;前端携带的流程ID可能已经过期,不能作为删除依据。
+ List pendingProcessIds = new ArrayList<>();
+ for (SysUser user : userList) {
+ if (!ObjectUtil.equals(user.getProcessStatus(), TaskCheckStatusEnum.PADDING.getCode())) {
+ continue;
+ }
+ if (StrUtil.isBlank(user.getProcessId())) {
+ throw new RuntimeException("审核中的用户缺少流程ID,无法删除");
+ }
+ pendingProcessIds.add(user.getProcessId());
+ }
+ if (CollUtil.isNotEmpty(pendingProcessIds)) {
+ TaskParamDto taskParam = new TaskParamDto();
+ taskParam.setProcessInstanceIdList(pendingProcessIds.stream().distinct().toList());
+ R stopResult = remoteFlowEngineService.stopProcessInstance(taskParam);
+ if (stopResult == null || !stopResult.isOk()) {
+ String errorMessage = stopResult == null ? null : stopResult.getMsg();
+ throw new RuntimeException(StrUtil.blankToDefault(errorMessage, "取消用户审批流程失败"));
+ }
+ }
+
// 删除 spring cache
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
for (SysUser sysUser : userList) {
@@ -435,9 +470,9 @@ public class SysUserServiceImpl extends ServiceImpl impl
this.removeBatchByIds(userIds);
// 删除钱包
CompletableFuture.runAsync(() -> {
- remoteWalletService.delWallet(List.of(ids));
+ remoteWalletService.delWallet(userIds);
});
- for (Long userId : ids) {
+ for (Long userId : userIds) {
remoteWalletService.userChange(userId, Boolean.TRUE);
}