4
0

11 کامیت‌ها 1211219d10 ... e2711bbb2e

نویسنده SHA1 پیام تاریخ
  dengjia e2711bbb2e draft: consistent async exports 1 ماه پیش
  dengjia c3ed76e1b0 Merge remote-tracking branch 'origin/feat-20250730-userexport' into feat-250729-moreExports 1 ماه پیش
  dengjia 2f495c9a10 feat: export user+type only allow 1 running with annotation 1 ماه پیش
  lixuesong dd21a94bac feat: 导出设置重试机制 1 ماه پیش
  lixuesong bb200b5409 refactor: exportResult抽取为公共的 1 ماه پیش
  lixuesong 6be8462d91 refactor: 人员导出-优化 1 ماه پیش
  lixuesong b0869673a3 fix: 人员导出-上传oss文件路径问题 1 ماه پیش
  lixuesong f2fce25bf7 fix: 人员导出-标记生成中的状态 1 ماه پیش
  lixuesong 41b628c174 fix: 人员导出-删除缓存文件 1 ماه پیش
  lixuesong c7cfea2b3a fix: 人员导出逻辑调整 1 ماه پیش
  lixuesong 4909188055 feat: 1.导出异步机制实现;2.人员导出逻辑编写 1 ماه پیش
26فایلهای تغییر یافته به همراه1225 افزوده شده و 6 حذف شده
  1. 5 0
      hnqz-common/hnqz-common-core/src/main/java/com/qunzhixinxi/hnqz/common/core/constant/CommonConstants.java
  2. 7 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/constant/CacheConstants.java
  3. 34 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/constant/enums/ExportType.java
  4. 17 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/dto/SysUserDTO.java
  5. 66 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/model/excel/SysUserExcelModel.java
  6. 20 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/aspect/ExportGuard.java
  7. 50 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/aspect/ExportGuardAspect.java
  8. 50 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/config/RetryConfiguration.java
  9. 15 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/config/UpmsConfig.java
  10. 8 4
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/init/InitController.java
  11. 2 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/user/SysUserController.java
  12. 69 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/user/SysUserExportController.java
  13. 26 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/event/TaskCheckSupExportEvent.java
  14. 34 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/event/UserExportEvent.java
  15. 37 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/listener/TaskCheckSupExportEventListener.java
  16. 36 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/listener/UserExportEventListener.java
  17. 22 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysCommonExportService.java
  18. 12 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysFileService.java
  19. 35 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysUserExportService.java
  20. 7 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/WmTaskSupervisionService.java
  21. 69 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysCommonExportServiceImpl.java
  22. 59 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysFileServiceImpl.java
  23. 377 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysUserExportServiceImpl.java
  24. 40 2
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/WmTaskSupervisionServiceImpl.java
  25. 93 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/util/ExportUtils.java
  26. 35 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/util/RedisUtils.java

+ 5 - 0
hnqz-common/hnqz-common-core/src/main/java/com/qunzhixinxi/hnqz/common/core/constant/CommonConstants.java

@@ -82,4 +82,9 @@ public interface CommonConstants {
 	 */
 	String IMAGE_CODE_TYPE = "blockPuzzle";
 
+	/**
+	 * 报告文件有效期
+	 */
+	long DEF_REPORT_TTL = 7L * 24 * 60 * 60 * 1000;
+
 }

+ 7 - 0
hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/constant/CacheConstants.java

@@ -99,6 +99,13 @@ public interface CacheConstants {
 
     String NEW_EXCEL_COMMON_REPORT_CACHE = "new_excel_export:common_report:%s";
 
+    String ASYNC_EXPORT_LIMIT_KEY = "async_export:limit";
+
+    /**
+     * 异步导出缓存key
+     */
+    String ASYNC_EXPORT_CACHE = "user_export:%s:%s";
+
 
     Long DEF_REPORT_CREATING_TTL = 24L * 60 * 60 * 1000;
 

+ 34 - 0
hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/constant/enums/ExportType.java

@@ -0,0 +1,34 @@
+package com.qunzhixinxi.hnqz.admin.api.constant.enums;
+
+import com.qunzhixinxi.hnqz.admin.api.constant.CacheConstants;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 导出类型
+ *
+ * @author snows
+ * @date 2025/07/30
+ */
+@Getter
+@AllArgsConstructor
+public enum ExportType {
+
+	USER("USER", "人员信息"),
+	CHECK_SUP("CHECK_SUP", "审核监督信息");
+
+	/**
+	 * 类型
+	 */
+	private final String type;
+
+	/**
+	 * 描述
+	 */
+	private final String description;
+
+	public static String getAsyncExportCache(ExportType exportType, Integer userId) {
+        return String.format(CacheConstants.ASYNC_EXPORT_CACHE, exportType.getType(), userId);
+    }
+}

+ 17 - 0
hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/dto/SysUserDTO.java

@@ -121,4 +121,21 @@ public final class SysUserDTO {
         private Integer current;
 
     }
+
+    @Data
+    public static class OnList {
+
+        private String username;
+
+        private String realname;
+
+        private List<Integer> role;
+
+        private Integer deptId;
+
+        private List<Long> areaCodes;
+
+        private String lockFlag;
+
+    }
 }

+ 66 - 0
hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/model/excel/SysUserExcelModel.java

@@ -0,0 +1,66 @@
+package com.qunzhixinxi.hnqz.admin.api.model.excel;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.annotation.write.style.ColumnWidth;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 用户 Excel 模型
+ *
+ * @author snows
+ * @date 2025/07/30
+ */
+@Data
+public class SysUserExcelModel implements Serializable {
+	private static final long serialVersionUID = 6204125117898666322L;
+
+	/**
+	 * 人员名称
+	 */
+	@ExcelProperty(value = "人员名称", index = 0)
+	@ColumnWidth(15)
+	private String realName;
+
+	/**
+	 * 手机号
+	 */
+	@ExcelProperty(value = "手机号", index = 1)
+	@ColumnWidth(20)
+	private String phoneNumber;
+
+	/**
+	 * 人员角色
+	 */
+	@ExcelProperty(value = "人员角色", index = 2)
+	@ColumnWidth(20)
+	private String roleList;
+
+	/**
+	 * 派工方
+	 */
+	@ExcelProperty(value = "派工方", index = 3)
+	@ColumnWidth(20)
+	private String deptName;
+
+	/**
+	 * 状态
+	 */
+	@ExcelProperty(value = "启停状态", index = 4)
+	private String lockFlag;
+
+	/**
+	 * 创建时间
+	 */
+	@ExcelProperty(value = "创建时间", index = 5)
+	@ColumnWidth(20)
+	private String createTime;
+
+	/**
+	 * 证书状态
+	 */
+	@ExcelProperty(value = "是/否备案", index = 6)
+	private String certificateFlag;
+
+}

+ 20 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/aspect/ExportGuard.java

@@ -0,0 +1,20 @@
+
+package com.qunzhixinxi.hnqz.admin.aspect;
+
+import java.lang.annotation.*;
+
+/**
+ * @author hnqz
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ExportGuard {
+
+	/**
+	 * enum ExportType
+	 * @return {String}
+	 */
+	String type();
+
+}

+ 50 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/aspect/ExportGuardAspect.java

@@ -0,0 +1,50 @@
+
+package com.qunzhixinxi.hnqz.admin.aspect;
+
+import com.qunzhixinxi.hnqz.admin.api.constant.enums.ExportType;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import cn.hutool.extra.spring.SpringUtil;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+/**
+ *
+ */
+@Slf4j
+@Aspect
+@AllArgsConstructor
+@Component
+public class ExportGuardAspect {
+
+	@SneakyThrows
+	@Around("@annotation(exportGuard)")
+	public Object around(ProceedingJoinPoint point, ExportGuard exportGuard) {
+		ExportType exportType = ExportType.valueOf(exportGuard.type());
+		if (point.getArgs().length < 1) {
+			throw new RuntimeException("第一个参数必须是用户对象");
+		}
+		HnqzUser user = (HnqzUser) point.getArgs()[0];
+		if (user == null) {
+			throw new RuntimeException("第一个参数必须是用户对象");
+		}
+		String key = ExportType.getAsyncExportCache(exportType, user.getId());
+		// RedisTemplateConfig
+		@SuppressWarnings("unchecked")
+		RedisTemplate<String, Object> redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getBean("redisTemplate");
+		String status = (String) redisTemplate.opsForValue().get(key);
+		if ("GENERATING".equals(status)) {
+			throw new RuntimeException("导出正在生成中,请稍后再试");
+		}
+		
+		return point.proceed();
+	}
+
+}

+ 50 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/config/RetryConfiguration.java

@@ -0,0 +1,50 @@
+package com.qunzhixinxi.hnqz.admin.config;
+
+import com.qunzhixinxi.hnqz.common.core.exception.BizException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.retry.annotation.EnableRetry;
+import org.springframework.retry.backoff.FixedBackOffPolicy;
+import org.springframework.retry.policy.SimpleRetryPolicy;
+import org.springframework.retry.support.RetryTemplate;
+
+import java.util.Collections;
+
+/**
+ * 重试配置
+ *
+ * @author snows
+ * @date 2025/08/04
+ */
+@EnableRetry
+@Configuration
+@RequiredArgsConstructor
+public class RetryConfiguration {
+
+	private final UpmsConfig upmsConfig;
+
+	/**
+	 * 重试模板
+	 *
+	 * @return {@link RetryTemplate } 重试模板实例
+	 */
+	@Bean
+	public RetryTemplate retryTemplate() {
+		// 构建重试模板实例
+		RetryTemplate retryTemplate = new RetryTemplate();
+
+		// 设置重试回退操作策略,主要设置重试间隔时间
+		FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
+		backOffPolicy.setBackOffPeriod(upmsConfig.getRetryBackOffPeriod());
+
+		// 设置重试策略,主要设置重试次数
+		SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(upmsConfig.getMaxRetryTimes(), Collections
+				.<Class<? extends Throwable>, Boolean>singletonMap(BizException.class, true));
+
+		retryTemplate.setRetryPolicy(retryPolicy);
+		retryTemplate.setBackOffPolicy(backOffPolicy);
+
+		return retryTemplate;
+	}
+}

+ 15 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/config/UpmsConfig.java

@@ -64,4 +64,19 @@ public class UpmsConfig {
 	 */
 	private Integer pltEntId;
 
+	/**
+	 * 异步导出限制个数(全局)
+	 */
+	private Integer asyncExportLimit = 3;
+
+	/**
+	 * 异步导出重试间隔时间(固定时间的退避策略)
+	 */
+	private Long retryBackOffPeriod = 60000L;
+
+	/**
+	 * 异步最大重试次数
+	 */
+	private Integer maxRetryTimes = 3;
+
 }

+ 8 - 4
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/init/InitController.java

@@ -1466,9 +1466,9 @@ public class InitController {
     return R.ok(r);
   }
 
-  @ResponseExcel(
+  /* @ResponseExcel(
       name = "taskCheckHistoryExcel",
-      sheets = {@Sheet(sheetName = "审核信息")})
+      sheets = {@Sheet(sheetName = "审核信息")}) */
   @GetMapping(value = "/cnbg/init/task/check/his/export")
   public List<TaskCheckHistoryExcelModel> exportTaskCheckHistoryExcel(
     @RequestParam(value = "taskPeriod", required = true) LocalDate[] taskPeriod) {
@@ -1481,8 +1481,12 @@ public class InitController {
       throw new RuntimeException("没有导出审核监督明细的权限");
     }
 
-    return wmTaskSupervisionService.selectTaskSupProcess(SecurityUtils.getUser(), taskPeriod);
-    
+    // sync
+    // return wmTaskSupervisionService.selectTaskSupProcess(SecurityUtils.getUser(), taskPeriod);
+
+    // async
+    wmTaskSupervisionService.asyncExportTaskCheckSup(SecurityUtils.getUser(), taskPeriod);
+    return List.of();
   }
 
   private Map<String, String> getInfo(

+ 2 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/user/SysUserController.java

@@ -36,6 +36,7 @@ import com.qunzhixinxi.hnqz.admin.service.SysRoleService;
 import com.qunzhixinxi.hnqz.admin.service.SysUserAreaService;
 import com.qunzhixinxi.hnqz.admin.service.SysUserService;
 import com.qunzhixinxi.hnqz.admin.service.WmPlatformQuizTestResultService;
+import com.qunzhixinxi.hnqz.admin.service.impl.SysUserExportServiceImpl;
 import com.qunzhixinxi.hnqz.common.core.constant.CommonConstants;
 import com.qunzhixinxi.hnqz.common.core.entity.BaseEntity;
 import com.qunzhixinxi.hnqz.common.core.exception.BizException;
@@ -1095,6 +1096,7 @@ public class SysUserController {
      *
      * @param query 查询参数列表
      * @return 用户集合
+     * @see SysUserExportServiceImpl#listUsers(HnqzUser, List, SysUserDTO.OnList) 查询条件如有修改,需要同步代码逻辑
      */
     @PostMapping(value = "/selectUserList")
     public R<?> pageUsers(@Validated @RequestBody SysUserDTO.OnPage query) {

+ 69 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/user/SysUserExportController.java

@@ -0,0 +1,69 @@
+package com.qunzhixinxi.hnqz.admin.controller.user;
+
+import com.qunzhixinxi.hnqz.admin.api.constant.CacheConstants;
+import com.qunzhixinxi.hnqz.admin.api.constant.enums.ExportType;
+import com.qunzhixinxi.hnqz.admin.api.dto.SysUserDTO;
+import com.qunzhixinxi.hnqz.admin.api.entity.WmReportOpt;
+import com.qunzhixinxi.hnqz.admin.api.model.excel.SysUserExcelModel;
+import com.qunzhixinxi.hnqz.admin.service.SysCommonExportService;
+import com.qunzhixinxi.hnqz.admin.aspect.ExportGuard;
+import com.qunzhixinxi.hnqz.admin.service.SysUserExportService;
+import com.qunzhixinxi.hnqz.common.core.util.R;
+import com.qunzhixinxi.hnqz.common.log.annotation.SysLog;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+import com.qunzhixinxi.hnqz.common.security.util.SecurityUtils;
+
+import cn.hutool.core.util.ArrayUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 用户导出控制器
+ *
+ * @author snows
+ * @date 2025/07/30
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping(value = "/user/export")
+public class SysUserExportController {
+
+	private final SysUserExportService userExportService;
+	private final SysCommonExportService commonExportService;
+
+	/**
+	 * 导出用户列表
+	 *
+	 * @return {@link List }<{@link SysUserExcelModel }> 用户信息
+	 */
+	@SysLog("导出用户信息")
+	@PostMapping("/export-user")
+	public R<Boolean> exportUser(@RequestBody SysUserDTO.OnList query) {
+		HnqzUser user = SecurityUtils.getUser();
+		if (!ArrayUtil.contains(user.getRoles(), 50)) { // 事业部系统管理员
+			throw new RuntimeException("没有导出人员权限");
+		}
+		return R.ok(userExportService.asyncExport(user, SecurityUtils.getRoles(), query));
+	}
+
+	/**
+	 * 导出用户信息的结果
+	 *
+	 * @return {@link WmReportOpt } 状态和结果
+	 */
+	@GetMapping("/export-user-result")
+	public R<WmReportOpt> exportResult() {
+		HnqzUser user = SecurityUtils.getUser();
+		String key = String.format(CacheConstants.ASYNC_EXPORT_CACHE, ExportType.USER.getType(), user.getId());
+
+		return R.ok(commonExportService.exportResult(user, key));
+	}
+}

+ 26 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/event/TaskCheckSupExportEvent.java

@@ -0,0 +1,26 @@
+package com.qunzhixinxi.hnqz.admin.event;
+
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.time.LocalDate;
+
+/**
+ * 任务审核监督导出事件
+ *
+ */
+@Getter
+@AllArgsConstructor
+public class TaskCheckSupExportEvent {
+
+	/**
+	 * 用户
+	 */
+	private HnqzUser user;
+
+	/**
+	 * 查询参数
+	 */
+	private LocalDate[] taskPeriod;
+}

+ 34 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/event/UserExportEvent.java

@@ -0,0 +1,34 @@
+package com.qunzhixinxi.hnqz.admin.event;
+
+import com.qunzhixinxi.hnqz.admin.api.dto.SysUserDTO;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.List;
+
+/**
+ * 用户导出事件
+ *
+ * @author snows
+ * @date 2025/07/30
+ */
+@Getter
+@AllArgsConstructor
+public class UserExportEvent {
+
+	/**
+	 * 用户
+	 */
+	private HnqzUser user;
+
+	/**
+	 * 角色
+	 */
+	private List<Integer> roles;
+
+	/**
+	 * 查询参数
+	 */
+	private SysUserDTO.OnList query;
+}

+ 37 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/listener/TaskCheckSupExportEventListener.java

@@ -0,0 +1,37 @@
+package com.qunzhixinxi.hnqz.admin.listener;
+
+import com.qunzhixinxi.hnqz.admin.event.TaskCheckSupExportEvent;
+import com.qunzhixinxi.hnqz.admin.event.UserExportEvent;
+import com.qunzhixinxi.hnqz.admin.service.SysUserExportService;
+import com.qunzhixinxi.hnqz.admin.service.WmTaskSupervisionService;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.annotation.Order;
+import org.springframework.scheduling.annotation.Async;
+
+/**
+ * 任务审核监督导出事件监听器
+ *
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Configuration
+public class TaskCheckSupExportEventListener {
+
+	private final WmTaskSupervisionService wmTaskSupervisionService;
+
+	/**
+	 * 推送事件
+	 *
+	 * @param event 事件
+	 */
+	@Async
+	@Order
+	@EventListener(TaskCheckSupExportEvent.class)
+	public void pushEvent(TaskCheckSupExportEvent event) {
+		wmTaskSupervisionService.selectTaskSupProcess(event.getUser(), event.getTaskPeriod());
+	}
+}

+ 36 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/listener/UserExportEventListener.java

@@ -0,0 +1,36 @@
+package com.qunzhixinxi.hnqz.admin.listener;
+
+import com.qunzhixinxi.hnqz.admin.event.UserExportEvent;
+import com.qunzhixinxi.hnqz.admin.service.SysUserExportService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.annotation.Order;
+import org.springframework.scheduling.annotation.Async;
+
+/**
+ * 用户导出事件监听器
+ *
+ * @author snows
+ * @date 2025/07/30
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Configuration
+public class UserExportEventListener {
+
+	private final SysUserExportService userExportService;
+
+	/**
+	 * 推送事件
+	 *
+	 * @param event 事件
+	 */
+	@Async
+	@Order
+	@EventListener(UserExportEvent.class)
+	public void pushEvent(UserExportEvent event) {
+		userExportService.export(event.getUser(), event.getRoles(), event.getQuery());
+	}
+}

+ 22 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysCommonExportService.java

@@ -0,0 +1,22 @@
+package com.qunzhixinxi.hnqz.admin.service;
+
+import com.qunzhixinxi.hnqz.admin.api.entity.WmReportOpt;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+
+/**
+ * 通用导出服务
+ *
+ * @author snows
+ * @date 2025/08/04
+ */
+public interface SysCommonExportService {
+
+	/**
+	 * 导出信息的结果
+	 *
+	 * @param user     用户
+	 * @param redisKey Redis key
+	 * @return {@link WmReportOpt } 状态和结果
+	 */
+	WmReportOpt exportResult(HnqzUser user, String redisKey);
+}

+ 12 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysFileService.java

@@ -7,6 +7,7 @@ import com.qunzhixinxi.hnqz.common.core.util.R;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.http.HttpServletResponse;
+import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
 import java.util.Map;
@@ -36,6 +37,17 @@ public interface SysFileService extends IService<SysFile> {
      */
     Map<String, String> upload(MultipartFile file, String username);
 
+    /**
+     * 上传
+     *
+     * @param inputStream      输入流
+     * @param originalFileName 原始文件名
+     * @param fileName         文件名
+     * @param username         用户名
+     * @return {@link Map }<{@link String }, {@link String }> 结果
+     */
+    Map<String, String> upload(InputStream inputStream, String originalFileName, String fileName, String username);
+
     /**
      * 上传文件
      *

+ 35 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysUserExportService.java

@@ -0,0 +1,35 @@
+package com.qunzhixinxi.hnqz.admin.service;
+
+import com.qunzhixinxi.hnqz.admin.api.dto.SysUserDTO;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+
+import java.util.List;
+
+/**
+ * 用户导出服务
+ *
+ * @author snows
+ * @date 2025/07/30
+ */
+public interface SysUserExportService {
+
+	/**
+	 * 异步导出
+	 *
+	 * @param user  用户
+	 * @param roles 角色
+	 * @param query 查询
+	 * @return {@link Boolean } 是否成功
+	 */
+	Boolean asyncExport(HnqzUser user, List<Integer> roles, SysUserDTO.OnList query);
+
+	/**
+	 * 导出用户
+	 *
+	 * @param user  用户
+	 * @param roles 角色
+	 * @param query 查询
+	 * @return {@link Boolean } 是否成功
+	 */
+	Boolean export(HnqzUser user, List<Integer> roles, SysUserDTO.OnList query);
+}

+ 7 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/WmTaskSupervisionService.java

@@ -59,5 +59,12 @@ public interface WmTaskSupervisionService extends IService<WmTaskSupervision> {
      * @return
      */
     List<TaskCheckHistoryExcelModel> selectTaskSupProcess(HnqzUser user,  LocalDate[] taskPeriod);
+
+    /**
+     * 异步导出
+     * @param user
+     * @param taskPeriod
+     */
+    void asyncExportTaskCheckSup(HnqzUser user, LocalDate[] taskPeriod);
 }
 

+ 69 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysCommonExportServiceImpl.java

@@ -0,0 +1,69 @@
+package com.qunzhixinxi.hnqz.admin.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.qunzhixinxi.hnqz.admin.api.entity.WmReportOpt;
+import com.qunzhixinxi.hnqz.admin.service.SysCommonExportService;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 通用导出服务impl
+ *
+ * @author snows
+ * @date 2025/08/04
+ */
+@Service
+@RequiredArgsConstructor
+public class SysCommonExportServiceImpl implements SysCommonExportService {
+
+	private final RedisTemplate<String, Object> redisTemplate;
+
+	/**
+	 * 导出信息的结果
+	 *
+	 * @param user     用户
+	 * @param redisKey Redis key
+	 * @return {@link WmReportOpt } 状态和结果
+	 */
+	@Override
+	public WmReportOpt exportResult(HnqzUser user, String redisKey) {
+		String o = (String) redisTemplate.opsForValue().get(redisKey);
+		WmReportOpt opt = new WmReportOpt();
+
+		if (StringUtils.isNotEmpty(o)) {
+			// 生成中的
+			if ("GENERATING".equals(o)) {
+				opt.setStatus(WmReportOpt.WmReportOptStatus.GENERATING);
+			}
+			// 生成失败的
+			else if (o.startsWith("ERROR")) {
+				opt.setStatus(WmReportOpt.WmReportOptStatus.ERROR);
+				opt.setErrorMsg(o.split(StrUtil.UNDERLINE)[1]);
+			}
+			// 生成失败的2
+			else if (!o.startsWith("/admin/sys-file")) {
+				opt.setStatus(WmReportOpt.WmReportOptStatus.ERROR);
+				opt.setErrorMsg(o);
+			}
+			// 成功的
+			else {
+				LocalDateTime now = LocalDateTime.now();
+				Long expire = redisTemplate.opsForValue().getOperations().getExpire(redisKey, TimeUnit.SECONDS);
+				opt.setStatus(WmReportOpt.WmReportOptStatus.GENERATED);
+				opt.setTtl(expire != null ? now.plusSeconds(expire) : now);
+				opt.setLatestUrl(o);
+			}
+
+		} else {
+			opt.setStatus(WmReportOpt.WmReportOptStatus.NOT_GENERATE);
+		}
+
+		return opt;
+	}
+}

+ 59 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysFileServiceImpl.java

@@ -124,6 +124,41 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile>
     return resultMap;
   }
 
+  /**
+   * 上传
+   *
+   * @param inputStream 输入流
+   * @param originalFileName 原始文件名
+   * @param fileName    文件名
+   * @param username    用户名
+   * @return {@link Map }<{@link String }, {@link String }> 结果
+   */
+  @Override
+  public Map<String, String> upload(InputStream inputStream, String originalFileName, String fileName, String username) {
+
+    // 判断是否有重名的操作
+    Map<String, String> resultMap = new HashMap<>(5);
+    resultMap.put("bucketName", ossProperties.getBucketName());
+    resultMap.put("orgFileName", fileName);
+    resultMap.put("fileName", fileName);
+
+    String url = String.format("/admin/sys-file/%s/%s", ossProperties.getBucketName(), fileName);
+    resultMap.put("url", url);
+
+    try {
+      minioTemplate.putObject(ossProperties.getBucketName(), fileName, inputStream);
+      // 文件管理数据记录,收集管理追踪文件(注意文件大小不保证准确性)
+      SysFile sysFile = this.fileLog(fileName, fileName, inputStream.available(), url, username);
+      resultMap.put("fileId", String.valueOf(sysFile.getId()));
+
+    } catch (Exception e) {
+      log.error("上传失败", e);
+      return null;
+    }
+
+    return resultMap;
+  }
+
   private MultipartFile heic2jpg(InputStream inputStream, String fileName) throws IOException {
 
     String tempFileName =
@@ -293,6 +328,30 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile>
     return sysFile;
   }
 
+  /**
+   * 文件管理数据记录,收集管理追踪文件
+   *
+   * @param originalFileName 原始文件名
+   * @param fileName         文件名
+   * @param size             大小
+   * @param url              文件url
+   * @param username         用户名
+   * @return {@link SysFile }
+   */
+  private SysFile fileLog(String originalFileName, String fileName, Integer size, String url, String username) {
+    SysFile sysFile = new SysFile();
+    // 原文件名
+    sysFile.setFileName(fileName);
+    sysFile.setOriginal(originalFileName);
+    sysFile.setFileSize(Long.valueOf(size));
+    sysFile.setType(FileUtil.extName(originalFileName));
+    sysFile.setBucketName(ossProperties.getBucketName());
+    sysFile.setPath(url);
+    sysFile.setCreateUser(username);
+    this.save(sysFile);
+    return sysFile;
+  }
+
   @Override
   public String uploadImgByByte(InputStream inputStream) {
     String fileName = IdUtil.simpleUUID() + StrUtil.DOT + ".jpg";

+ 377 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysUserExportServiceImpl.java

@@ -0,0 +1,377 @@
+package com.qunzhixinxi.hnqz.admin.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DatePattern;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.EasyExcel;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.qunzhixinxi.hnqz.admin.api.constant.CacheConstants;
+import com.qunzhixinxi.hnqz.admin.api.constant.UpmsType;
+import com.qunzhixinxi.hnqz.admin.api.constant.enums.ExportType;
+import com.qunzhixinxi.hnqz.admin.api.dto.SysUserDTO;
+import com.qunzhixinxi.hnqz.admin.api.entity.SysAreaEntity;
+import com.qunzhixinxi.hnqz.admin.api.entity.SysDept;
+import com.qunzhixinxi.hnqz.admin.api.entity.SysRole;
+import com.qunzhixinxi.hnqz.admin.api.entity.SysUser;
+import com.qunzhixinxi.hnqz.admin.api.entity.SysUserCertificate;
+import com.qunzhixinxi.hnqz.admin.api.entity.SysUserRole;
+import com.qunzhixinxi.hnqz.admin.api.entity.WmReportOpt;
+import com.qunzhixinxi.hnqz.admin.api.model.excel.SysUserExcelModel;
+import com.qunzhixinxi.hnqz.admin.aspect.ExportGuard;
+import com.qunzhixinxi.hnqz.admin.config.UpmsConfig;
+import com.qunzhixinxi.hnqz.admin.controller.user.SysUserController;
+import com.qunzhixinxi.hnqz.admin.event.UserExportEvent;
+import com.qunzhixinxi.hnqz.admin.mapper.SysAreaEntityMapper;
+import com.qunzhixinxi.hnqz.admin.mapper.SysDeptMapper;
+import com.qunzhixinxi.hnqz.admin.mapper.SysRoleMapper;
+import com.qunzhixinxi.hnqz.admin.mapper.SysUserCertificateMapper;
+import com.qunzhixinxi.hnqz.admin.mapper.SysUserMapper;
+import com.qunzhixinxi.hnqz.admin.mapper.SysUserRoleMapper;
+import com.qunzhixinxi.hnqz.admin.service.SysFileService;
+import com.qunzhixinxi.hnqz.admin.service.SysUserAreaService;
+import com.qunzhixinxi.hnqz.admin.service.SysUserExportService;
+import com.qunzhixinxi.hnqz.admin.util.ExportUtils;
+import com.qunzhixinxi.hnqz.admin.util.OsEnvUtils;
+import com.qunzhixinxi.hnqz.admin.util.RedisUtils;
+import com.qunzhixinxi.hnqz.common.core.constant.CommonConstants;
+import com.qunzhixinxi.hnqz.common.core.exception.BizException;
+import com.qunzhixinxi.hnqz.common.core.util.SpringContextHolder;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 用户导出服务 impl
+ *
+ * @author snows
+ * @date 2025/07/30
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SysUserExportServiceImpl implements SysUserExportService {
+	private final RedisTemplate<String, Object> redisTemplate;
+	private final SysAreaEntityMapper areaEntityMapper;
+	private final SysUserMapper userMapper;
+	private final SysRoleMapper roleMapper;
+	private final SysUserRoleMapper userRoleMapper;
+	private final SysUserAreaService userAreaService;
+	private final SysDeptMapper deptMapper;
+	private final SysUserCertificateMapper userCertificateMapper;
+	private final UpmsConfig upmsConfig;
+	private final RetryTemplate retryTemplate;
+	private final RedisUtils redisUtils;
+	private final ExportUtils exportUtils;
+
+	/**
+	 * 异步导出
+	 * 设置重试次数和重试间隔
+	 *
+	 * @param user  用户
+	 * @param roles 角色
+	 * @param query 查询
+	 * @return {@link Boolean } 是否成功
+	 */
+	@Override
+	@ExportGuard(type = "USER")
+	public Boolean asyncExport(HnqzUser user, List<Integer> roles, SysUserDTO.OnList query) {
+
+		redisUtils.setExportStarting(ExportType.USER, user.getId());
+		SpringContextHolder.getApplicationContext().publishEvent(new UserExportEvent(user, roles, query));
+		return true;	
+	}
+
+	/**
+	 * 导出用户
+	 *
+	 * @param user  用户
+	 * @param roles 角色
+	 * @param query 查询
+	 * @return {@link Boolean } 是否成功
+	 */
+	@Override
+	public Boolean export(HnqzUser user, List<Integer> roles, SysUserDTO.OnList query) {
+		try {
+			return retryTemplate.execute(retryContext -> {
+				log.info("人员异步导出第{}次重试", retryContext.getRetryCount());
+
+				redisUtils.checkExportGlobalAllows(upmsConfig.getAsyncExportLimit());
+
+				// 查询用户列表 构建Excel数据
+				List<SysUser> users = this.listUsers(user, roles, query);
+				List<SysUserExcelModel> data = buildExcelModel(users);
+
+				// 导出
+				String resultValue = exportUtils.WriteExportExcel(ExportType.USER, data, SysUserExcelModel.class, user);
+				return !StrUtil.startWith(resultValue, "ERROR");
+			});
+
+		} finally {
+			// 释放全局限流
+			redisTemplate.opsForValue().decrement(CacheConstants.ASYNC_EXPORT_LIMIT_KEY);
+		}
+	}
+	
+
+	private List<SysUserExcelModel> buildExcelModel(List<SysUser> users) {
+		if (CollUtil.isEmpty(users)) {
+			log.info("用户列表为空");
+			return List.of();
+		}
+		// 查询企业信息
+		Map<Integer, SysDept> deptMap = queryDeptInfo(users);
+
+		// 查询角色信息
+		Map<Integer, String> roleMap = queryRoleInfo(users);
+		Map<Integer, List<SysUserRole>> userRolesMap = queryUserRoles(users);
+
+		// 查询备案信息
+		Map<Integer, Long> userCertMap = queryCertificateInfo(users);
+
+		// 转为excel列表
+		List<SysUserExcelModel> excelModels = users.stream().map(u -> {
+			SysUserExcelModel excelModel = BeanUtil.copyProperties(u, SysUserExcelModel.class, "lockFlag");
+			excelModel.setRealName(u.getRealname());
+			excelModel.setPhoneNumber(u.getPhone());
+			// 锁定状态
+			excelModel.setLockFlag("0".equals(u.getLockFlag()) ? "活跃" : "休眠");
+			// 创建时间
+			excelModel.setCreateTime(u.getCreateTime().format(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
+
+			// 派工方
+			SysDept dept = deptMap.get(u.getDeptId());
+			if (dept != null) {
+				excelModel.setDeptName(dept.getName());
+			}
+
+			// 角色
+			List<SysUserRole> userRoleList = userRolesMap.get(u.getUserId());
+			if (CollUtil.isNotEmpty(userRoleList)) {
+				String roleNames = userRoleList.stream()
+						.map(userRole -> roleMap.getOrDefault(userRole.getRoleId(), ""))
+						.collect(Collectors.joining(","));
+				excelModel.setRoleList(roleNames);
+			}
+
+			// 备案状态
+			long certCount = userCertMap.getOrDefault(u.getUserId(), 0L);
+			excelModel.setCertificateFlag(certCount > 0 ? "是" : "否");
+
+			return excelModel;
+		}).collect(Collectors.toList());
+
+		return excelModels;
+	}
+
+	/**
+	 * 查询部门信息
+	 */
+	private Map<Integer, SysDept> queryDeptInfo(List<SysUser> users) {
+		Set<Integer> deptIds = users.stream().map(SysUser::getDeptId).collect(Collectors.toSet());
+		Map<Integer, SysDept> deptMap = new HashMap<>();
+		if (CollUtil.isNotEmpty(deptIds)) {
+			List<SysDept> sysDepts = deptMapper.selectBatchIds(deptIds);
+			deptMap.putAll(sysDepts.stream().collect(Collectors.toMap(SysDept::getDeptId, Function.identity())));
+		}
+		return deptMap;
+	}
+
+	/**
+	 * 查询角色信息
+	 */
+	private Map<Integer, String> queryRoleInfo(List<SysUser> users) {
+		Set<Integer> userIds = users.stream().map(SysUser::getUserId).collect(Collectors.toSet());
+		List<SysUserRole> userRoles = userRoleMapper.selectList(Wrappers.<SysUserRole>lambdaQuery().in(SysUserRole::getUserId, userIds));
+
+		Map<Integer, String> roleMap = new HashMap<>();
+		if (CollUtil.isNotEmpty(userRoles)) {
+			Set<Integer> roleIds = userRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet());
+			List<SysRole> resultRoles = roleMapper.selectBatchIds(roleIds);
+			if (CollUtil.isNotEmpty(resultRoles)) {
+				roleMap.putAll(resultRoles.stream().collect(Collectors.toMap(SysRole::getRoleId, SysRole::getRoleName)));
+			}
+		}
+		return roleMap;
+	}
+
+	/**
+	 * 查询用户角色映射
+	 */
+	private Map<Integer, List<SysUserRole>> queryUserRoles(List<SysUser> users) {
+		Set<Integer> userIds = users.stream().map(SysUser::getUserId).collect(Collectors.toSet());
+		List<SysUserRole> userRoles = userRoleMapper.selectList(Wrappers.<SysUserRole>lambdaQuery().in(SysUserRole::getUserId, userIds));
+		return userRoles.stream().collect(Collectors.groupingBy(SysUserRole::getUserId));
+	}
+
+	/**
+	 * 查询备案信息
+	 */
+	private Map<Integer, Long> queryCertificateInfo(List<SysUser> users) {
+		Set<Integer> userIds = users.stream().map(SysUser::getUserId).collect(Collectors.toSet());
+		List<SysUserCertificate> userCertificates =
+				userCertificateMapper.selectList(Wrappers.<SysUserCertificate>lambdaQuery()
+						.in(SysUserCertificate::getUserId, userIds)
+						.eq(SysUserCertificate::getType, "REG")
+						.select(SysUserCertificate::getCertificateId, SysUserCertificate::getUserId));
+
+		return userCertificates.stream()
+				.collect(Collectors.groupingBy(SysUserCertificate::getUserId, Collectors.counting()));
+	}
+
+
+
+	/**
+	 * 列出用户
+	 *
+	 * @param user  用户
+	 * @param roles 角色
+	 * @param query 查询
+	 * @return {@link List<SysUser> } 用户列表
+	 * @see SysUserController#pageUsers(SysUserDTO.OnPage) 如有修改,需要同步代码逻辑
+	 */
+	private List<SysUser> listUsers(HnqzUser user, List<Integer> roles, SysUserDTO.OnList query) {
+		//  获取全部的可用角色
+		List<SysRole> queryRoles = roleMapper.selectList(Wrappers.emptyWrapper());
+		Map<Integer, SysRole> roleId2RoleMap = queryRoles
+				.stream().collect(Collectors.toMap(SysRole::getRoleId, Function.identity()));
+
+		// 获取操作人的角色
+		Set<Integer> operatorRoleIds = new HashSet<>(roles);
+
+		Set<Integer> targetRoleIds = new HashSet<>();
+
+
+		SysRole role = null;
+		boolean needArea = false;
+
+		// 系统管理员
+		if (operatorRoleIds.contains(1)) {
+			role = roleId2RoleMap.get(1);
+		}
+		// 中生平台管理员
+		else if (operatorRoleIds.contains(2)) {
+			role = roleId2RoleMap.get(2);
+		}
+		// 事业部系统管理员
+		else if (operatorRoleIds.contains(50)) {
+			role = roleId2RoleMap.get(50);
+		}
+		// 运营管理员
+		else if (operatorRoleIds.contains(3)) {
+			role = roleId2RoleMap.get(3);
+		}
+		// 区域管理员
+		else if (operatorRoleIds.contains(4)) {
+			role = roleId2RoleMap.get(4);
+			query.setDeptId(user.getDeptId());
+			needArea = true;
+		}
+		// 服务商管理员
+		else if (operatorRoleIds.contains(37)) {
+			role = roleId2RoleMap.get(37);
+			query.setDeptId(user.getDeptId());
+			needArea = true;
+		}
+		// 备案管理员
+		else if (operatorRoleIds.contains(47)) {
+			role = roleId2RoleMap.get(47);
+		}
+
+
+		if (role == null) {
+			log.error("当前用户角色禁止查看其他角色");
+			return Collections.emptyList();
+		} else {
+			String visible = role.getVisible();
+			if (StrUtil.isBlank(visible)) {
+				log.error("当前角色未配置查询角色");
+				return Collections.emptyList();
+			}
+
+			Set<Integer> visibles = Arrays.stream(visible.split(StrUtil.COMMA)).mapToInt(Integer::valueOf).boxed().collect(Collectors.toSet());
+
+			Integer targetRoleId = CollUtil.isEmpty(query.getRole()) ? null : query.getRole().get(0);
+
+			if (targetRoleId == null) {
+				targetRoleIds.addAll(visibles);
+			} else if (visibles.contains(targetRoleId)) {
+				targetRoleIds.add(targetRoleId);
+			} else {
+				log.error("当前角色禁止查看其他角色");
+				return Collections.emptyList();
+			}
+		}
+
+		// 获取所有三级区域
+		String areaCacheKey = "sys:area:lv3";
+		Set<Long> areaEntities;
+		if (Boolean.TRUE.equals(redisTemplate.hasKey(areaCacheKey))) {
+			areaEntities = (Set<Long>) redisTemplate.opsForValue().get(areaCacheKey);
+		} else {
+			areaEntities = areaEntityMapper.selectList(Wrappers.<SysAreaEntity>lambdaQuery().eq(SysAreaEntity::getAreaType, UpmsType.AreaType.DISTRICT))
+					.stream().map(SysAreaEntity::getAreaId).collect(Collectors.toSet());
+			redisTemplate.opsForValue().set(areaCacheKey, areaEntities, 12, TimeUnit.HOURS);
+		}
+
+		List<Long> areas = query.getAreaCodes();
+
+		if (CollUtil.isEmpty(areas) && needArea) {
+			areas = userAreaService.listUserAreas(Long.valueOf(user.getId()));
+		}
+
+		// 如果实际三级区域于查询三级区域相等,也就是全国的时候,默认直接查询全国
+		query.setAreaCodes((areaEntities.size() == (CollUtil.isNotEmpty(areas) ? areas.size() : 0)) ? Collections.emptyList() : new LinkedList<>(CollUtil.intersectionDistinct(areaEntities, areas)));
+		query.setRole(new LinkedList<>(targetRoleIds));
+
+		Set<Integer> areaUserIds = null;
+		if (CollUtil.isNotEmpty(query.getAreaCodes())) {
+			areaUserIds = userAreaService.listAreaUser(query.getAreaCodes()).stream().map(Long::intValue).collect(Collectors.toSet());
+		}
+
+		List<SysUserRole> userRoles = userRoleMapper.selectList(Wrappers.<SysUserRole>lambdaQuery()
+				.in(CollUtil.isNotEmpty(areaUserIds), SysUserRole::getUserId, areaUserIds)
+				.in(SysUserRole::getRoleId, query.getRole()));
+
+
+		Map<Integer, List<Integer>> collect = userRoles.stream()
+				.collect(Collectors.groupingBy(SysUserRole::getUserId, Collectors.mapping(SysUserRole::getRoleId, Collectors.toList())));
+
+
+		if (CollUtil.isEmpty(collect)) {
+			return Collections.emptyList();
+		}
+
+		LambdaQueryWrapper<SysUser> queryWrapper = Wrappers.<SysUser>lambdaQuery()
+				.like(StrUtil.isNotBlank(query.getUsername()), SysUser::getUsername, query.getUsername())
+				.like(StrUtil.isNotBlank(query.getRealname()), SysUser::getRealname, query.getRealname())
+				.eq(StrUtil.isNotBlank(query.getLockFlag()), SysUser::getLockFlag, query.getLockFlag())
+				.eq(query.getDeptId() != null, SysUser::getDeptId, query.getDeptId())
+				.in(SysUser::getUserId, collect.keySet())
+				.orderByDesc(SysUser::getCreateTime);
+
+		return userMapper.selectList(queryWrapper);
+	}
+
+}

+ 40 - 2
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/WmTaskSupervisionServiceImpl.java

@@ -4,21 +4,31 @@ import cn.hutool.core.collection.CollUtil;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.qunzhixinxi.hnqz.admin.api.constant.UpmsState;
+import com.qunzhixinxi.hnqz.admin.api.constant.enums.ExportType;
 import com.qunzhixinxi.hnqz.admin.api.dto.WmSupervisionDto;
 import com.qunzhixinxi.hnqz.admin.api.entity.WmTaskSupervision;
 import com.qunzhixinxi.hnqz.admin.api.model.excel.TaskCheckHistoryExcelModel;
+import com.qunzhixinxi.hnqz.admin.aspect.ExportGuard;
+import com.qunzhixinxi.hnqz.admin.config.UpmsConfig;
+import com.qunzhixinxi.hnqz.admin.event.TaskCheckSupExportEvent;
 import com.qunzhixinxi.hnqz.admin.mapper.WmTaskSupervisionMapper;
 import com.qunzhixinxi.hnqz.admin.service.WmTaskSupervisionService;
+import com.qunzhixinxi.hnqz.admin.util.ExportUtils;
+import com.qunzhixinxi.hnqz.admin.util.RedisUtils;
+import com.qunzhixinxi.hnqz.common.core.util.SpringContextHolder;
 import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
 import com.qunzhixinxi.hnqz.common.security.util.SecurityUtils;
 import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.retry.support.RetryTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -33,6 +43,11 @@ import java.util.List;
 public class WmTaskSupervisionServiceImpl extends ServiceImpl<WmTaskSupervisionMapper, WmTaskSupervision> implements WmTaskSupervisionService {
 
     private final WmTaskSupervisionMapper wmTaskSupervisionMapper;
+	private final RetryTemplate retryTemplate;
+	private final RedisUtils redisUtils;
+	private final UpmsConfig upmsConfig;
+    private final ExportUtils exportUtils;
+
     /**
      * 创建审核
      *
@@ -126,9 +141,32 @@ public class WmTaskSupervisionServiceImpl extends ServiceImpl<WmTaskSupervisionM
     }
 
     @Override
-    // @ExportGuard
+    @ExportGuard(type = "CHECK_SUP")
+    public void asyncExportTaskCheckSup(HnqzUser user, LocalDate[] taskPeriod) {
+
+        redisUtils.setExportStarting(ExportType.CHECK_SUP, user.getId());
+        SpringContextHolder.getApplicationContext().publishEvent(new TaskCheckSupExportEvent(user, taskPeriod));
+    }
+
+    @Override
     public List<TaskCheckHistoryExcelModel> selectTaskSupProcess(HnqzUser user,  LocalDate[] taskPeriod) {
-        List<TaskCheckHistoryExcelModel> t = wmTaskSupervisionMapper.selectTaskSupProcess(LocalDateTime.of(taskPeriod[0], LocalTime.MIN), LocalDateTime.of(taskPeriod[1], LocalTime.MAX));
+        List<TaskCheckHistoryExcelModel> t = List.of();
+        try {
+            t = retryTemplate.execute(retryContext -> {
+                log.info("人员异步导出第{}次重试", retryContext.getRetryCount());
+
+                redisUtils.checkExportGlobalAllows(upmsConfig.getAsyncExportLimit());
+                List<TaskCheckHistoryExcelModel> data = 
+                    wmTaskSupervisionMapper.selectTaskSupProcess(LocalDateTime.of(taskPeriod[0], LocalTime.MIN), LocalDateTime.of(taskPeriod[1], LocalTime.MAX));
+                // 导出
+                exportUtils.WriteExportExcel(ExportType.CHECK_SUP, data, TaskCheckHistoryExcelModel.class, user);
+
+                return data;
+            });
+		} finally {
+            redisUtils.releaseExportGlobalAllows();
+		}
+
         return t;
     }
 }

+ 93 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/util/ExportUtils.java

@@ -0,0 +1,93 @@
+package com.qunzhixinxi.hnqz.admin.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DatePattern;
+import com.alibaba.excel.EasyExcel;
+import com.qunzhixinxi.hnqz.admin.api.constant.enums.ExportType;
+import com.qunzhixinxi.hnqz.admin.service.SysFileService;
+import com.qunzhixinxi.hnqz.common.core.constant.CommonConstants;
+import com.qunzhixinxi.hnqz.common.security.service.HnqzUser;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * async export 工具类
+ *
+ */
+@Slf4j
+@Component
+public final class ExportUtils {
+
+  @Resource private RedisTemplate<String, Object> redisTemplate;
+  @Resource private SysFileService fileService;
+
+  public static final String ERROR_MSG_UNKNOWN = "ERROR_未知错误,请联系管理员";
+	public static final String ERROR_MSG_NO_DATA = "ERROR_没有数据";
+	public static final String ERROR_MSG_UPLOAD_FAIL = "ERROR_上传OSS失败";
+
+	public String WriteExportExcel(ExportType exportType, List<?> data, Class head, HnqzUser user) {
+		// 临时文件路径
+		String tempPath = OsEnvUtils.getEachEnvPaths().get(OsEnvUtils.TargetFile.TEMP.getName());
+		// 缓存文件名
+		String fileName = exportType.getDescription() + DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN)
+				.format(LocalDateTime.now()) + RandomStringUtils.randomNumeric(6) + ".xlsx";
+		String fullPath = tempPath + fileName;
+
+		String ret = ERROR_MSG_UNKNOWN;
+		try {
+			// 写入excel文件
+			EasyExcel.write(fullPath, head).sheet(exportType.getDescription())
+					.doWrite(data);
+			log.info("{}导出生成缓存文件:{}", exportType.getDescription(), fullPath);
+
+			// 上传oss
+			try (FileInputStream inputStream = new FileInputStream(fullPath)) {
+				Map<String, String> uploadResult = fileService.upload(inputStream, fileName, fileName, user.getUsername());
+				log.info("{}导出生成oss文件:{}", exportType.getDescription(), uploadResult);
+
+				if (CollUtil.isNotEmpty(uploadResult)) {
+					ret = uploadResult.get("url");
+				} else {
+					ret = ERROR_MSG_UPLOAD_FAIL;
+				}
+			}
+		} catch (Exception e) {
+			log.error(exportType.getDescription() + "导出失败", e);
+			// 清理临时文件
+			cleanupTempFile(fullPath);
+			ret = ERROR_MSG_UNKNOWN;
+		}
+
+		// 缓存key
+		String key = ExportType.getAsyncExportCache(exportType, user.getId());
+		redisTemplate.opsForValue().set(key, ret, CommonConstants.DEF_REPORT_TTL, TimeUnit.MILLISECONDS);
+		return ret;
+	}
+
+  	/**
+	 * 清理临时文件
+	 */
+	private void cleanupTempFile(String fullPath) {
+		try {
+			File file = new File(fullPath);
+			if (file.exists()) {
+				file.delete();
+			}
+		} catch (Exception e) {
+			log.warn("删除临时文件失败: {}", fullPath, e);
+		}
+	}
+
+}

+ 35 - 0
hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/util/RedisUtils.java

@@ -2,6 +2,11 @@ package com.qunzhixinxi.hnqz.admin.util;
 
 import cn.hutool.core.collection.ListUtil;
 import cn.hutool.core.util.IdUtil;
+
+import com.qunzhixinxi.hnqz.admin.api.constant.CacheConstants;
+import com.qunzhixinxi.hnqz.admin.api.constant.enums.ExportType;
+import com.qunzhixinxi.hnqz.admin.api.entity.WmReportOpt;
+import com.qunzhixinxi.hnqz.common.core.constant.CommonConstants;
 import com.qunzhixinxi.hnqz.common.core.exception.BizException;
 import java.time.LocalDateTime;
 import java.util.Collections;
@@ -61,6 +66,36 @@ public final class RedisUtils {
     return new Token(token, value, LocalDateTime.now().plusSeconds(DEF_DURATION));
   }
 
+  public void checkExportGlobalAllows(int max) {
+    // 使用Redis原子操作实现限流
+			String key = CacheConstants.ASYNC_EXPORT_LIMIT_KEY;
+			Long increment = redisTemplate.opsForValue().increment(key);
+
+			// 设置key的过期时间,避免计数器无限增长
+			if (increment != null && increment.equals(1L)) {
+				redisTemplate.expire(key, 1, TimeUnit.MINUTES);
+			}
+
+			// 检查是否超过限流阈值
+			if (increment != null && increment > max) {
+				// 超过限流阈值时,减少计数器并抛出异常
+				redisTemplate.opsForValue().decrement(key);
+				throw new BizException("系统繁忙,请稍后再试");
+			}
+  }
+
+  public void setExportStarting(ExportType exportType, Integer userId) {
+      // 缓存key
+			String cacheKey = ExportType.getAsyncExportCache(exportType, userId);
+			// 更新状态为生成中
+			redisTemplate.opsForValue().set(cacheKey, WmReportOpt.WmReportOptStatus.GENERATING.name(), CommonConstants.DEF_REPORT_TTL, TimeUnit.MILLISECONDS);
+  }
+
+  public void releaseExportGlobalAllows() {
+      // 释放全局限流
+		  redisTemplate.opsForValue().decrement(CacheConstants.ASYNC_EXPORT_LIMIT_KEY);
+  }
+
   @Data
   @AllArgsConstructor
   public static class Token{