Browse Source

Merge remote-tracking branch 'origin/feat-20250730-userexport' into feat-250729-moreExports

dengjia 4 days ago
parent
commit
c3ed76e1b0
18 changed files with 1050 additions and 0 deletions
  1. 7 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/constant/CacheConstants.java
  2. 33 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/constant/enums/ExportType.java
  3. 17 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/dto/SysUserDTO.java
  4. 66 0
      hnqz-upms/hnqz-upms-api/src/main/java/com/qunzhixinxi/hnqz/admin/api/model/excel/SysUserExcelModel.java
  5. 20 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/aspect/ExportGuard.java
  6. 50 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/aspect/ExportGuardAspect.java
  7. 50 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/config/RetryConfiguration.java
  8. 15 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/config/UpmsConfig.java
  9. 2 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/user/SysUserController.java
  10. 69 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/controller/user/SysUserExportController.java
  11. 34 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/event/UserExportEvent.java
  12. 36 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/listener/UserExportEventListener.java
  13. 22 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysCommonExportService.java
  14. 12 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysFileService.java
  15. 35 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/SysUserExportService.java
  16. 69 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysCommonExportServiceImpl.java
  17. 59 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysFileServiceImpl.java
  18. 454 0
      hnqz-upms/hnqz-upms-biz/src/main/java/com/qunzhixinxi/hnqz/admin/service/impl/SysUserExportServiceImpl.java

+ 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;
 

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

@@ -0,0 +1,33 @@
+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", "人员信息");
+
+	/**
+	 * 类型
+	 */
+	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;
+
 }

+ 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));
+	}
+}

+ 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;
+}

+ 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);
+}

+ 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";

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

@@ -0,0 +1,454 @@
+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.OsEnvUtils;
+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 SysFileService fileService;
+	private final UpmsConfig upmsConfig;
+	private final RetryTemplate retryTemplate;
+
+	private static final long DEF_REPORT_TTL = 7L * 24 * 60 * 60 * 1000;
+	private static final String ERROR_MSG_UNKNOWN = "ERROR_未知错误,请联系管理员";
+	private static final String ERROR_MSG_NO_DATA = "ERROR_没有数据";
+	private static final String ERROR_MSG_UPLOAD_FAIL = "ERROR_上传OSS失败";
+
+
+	/**
+	 * 异步导出
+	 * 设置重试次数和重试间隔
+	 *
+	 * @param user  用户
+	 * @param roles 角色
+	 * @param query 查询
+	 * @return {@link Boolean } 是否成功
+	 */
+	@Override
+	@ExportGuard(type = "USER")
+	public Boolean asyncExport(HnqzUser user, List<Integer> roles, SysUserDTO.OnList query) {
+		return retryTemplate.execute(retryContext -> {
+			log.info("人员异步导出第{}次重试", retryContext.getRetryCount());
+
+			// 使用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 > upmsConfig.getAsyncExportLimit()) {
+				// 超过限流阈值时,减少计数器并抛出异常
+				redisTemplate.opsForValue().decrement(key);
+				throw new BizException("系统繁忙,请稍后再试");
+			}
+
+			// 缓存key
+			String cacheKey = String.format(CacheConstants.ASYNC_EXPORT_CACHE, ExportType.USER.getType(), user.getId());
+			// 更新状态为生成中
+			redisTemplate.opsForValue().set(cacheKey, WmReportOpt.WmReportOptStatus.GENERATING.name(), DEF_REPORT_TTL, TimeUnit.MILLISECONDS);
+
+			SpringContextHolder.getApplicationContext().publishEvent(new UserExportEvent(user, roles, query));
+			return Boolean.TRUE;
+		});
+
+	}
+
+	/**
+	 * 导出用户
+	 *
+	 * @param user  用户
+	 * @param roles 角色
+	 * @param query 查询
+	 * @return {@link Boolean } 是否成功
+	 */
+	@Override
+	public Boolean export(HnqzUser user, List<Integer> roles, SysUserDTO.OnList query) {
+		// 缓存key
+		String key = String.format(CacheConstants.ASYNC_EXPORT_CACHE, ExportType.USER.getType(), user.getId());
+
+		// 临时文件路径
+		String tempPath = OsEnvUtils.getEachEnvPaths().get(OsEnvUtils.TargetFile.TEMP.getName());
+		// 缓存文件名
+		String fileName = "人员_" + DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN)
+				.format(LocalDateTime.now()) + RandomStringUtils.randomNumeric(6) + ".xlsx";
+		String fullPath = tempPath + fileName;
+
+		try {
+			// 查询用户列表
+			List<SysUser> users = this.listUsers(user, roles, query);
+
+			if (CollUtil.isEmpty(users)) {
+				log.info("用户列表为空");
+				redisTemplate.opsForValue().set(key, ERROR_MSG_NO_DATA, DEF_REPORT_TTL, TimeUnit.MILLISECONDS);
+				return Boolean.FALSE;
+			}
+
+			// 构建Excel数据并导出
+			String resultValue = buildAndExportExcel(users, fullPath, fileName, user);
+			redisTemplate.opsForValue().set(key, resultValue, DEF_REPORT_TTL, TimeUnit.MILLISECONDS);
+
+			return !StrUtil.startWith(resultValue, "ERROR");
+		} finally {
+			// 清理临时文件
+			cleanupTempFile(fullPath);
+
+			// 删除限流key
+			String limitKey = CacheConstants.ASYNC_EXPORT_LIMIT_KEY;
+			redisTemplate.delete(limitKey);
+		}
+	}
+
+	/**
+	 * 构建Excel数据并导出到文件
+	 *
+	 * @param users 用户列表
+	 * @param fullPath 完整文件路径
+	 * @param fileName 文件名
+	 * @param user 当前用户
+	 * @return 结果值(URL或错误信息)
+	 */
+	private String buildAndExportExcel(List<SysUser> users, String fullPath, String fileName, HnqzUser user) {
+		try {
+			// 查询企业信息
+			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());
+
+			// 写入excel文件
+			EasyExcel.write(fullPath, SysUserExcelModel.class).sheet("人员")
+					.doWrite(excelModels);
+			log.info("人员导出生成缓存文件:{}", fullPath);
+
+			// 上传oss
+			try (FileInputStream inputStream = new FileInputStream(fullPath)) {
+				Map<String, String> uploadResult = fileService.upload(inputStream, fileName, fileName, user.getUsername());
+				log.info("人员导出生成oss文件:{}", uploadResult);
+
+				if (CollUtil.isNotEmpty(uploadResult)) {
+					return uploadResult.get("url");
+				} else {
+					return ERROR_MSG_UPLOAD_FAIL;
+				}
+			}
+		} catch (Exception e) {
+			log.error("人员导出失败", e);
+			return ERROR_MSG_UNKNOWN;
+		}
+	}
+
+	/**
+	 * 查询部门信息
+	 */
+	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()));
+	}
+
+	/**
+	 * 清理临时文件
+	 */
+	private void cleanupTempFile(String fullPath) {
+		try {
+			File file = new File(fullPath);
+			if (file.exists()) {
+				file.delete();
+			}
+		} catch (Exception e) {
+			log.warn("删除临时文件失败: {}", fullPath, e);
+		}
+	}
+
+
+	/**
+	 * 列出用户
+	 *
+	 * @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);
+	}
+
+}