Sfoglia il codice sorgente

接口请求逻辑优化

DESKTOP-6LTVLN7\Liumouren 1 mese fa
parent
commit
fa88cce90b
21 ha cambiato i file con 1708 aggiunte e 297 eliminazioni
  1. 69 0
      src/main/java/com/yykj/sjnmtybt/common/FlexibleEpochDeserializer.java
  2. 4 0
      src/main/java/com/yykj/sjnmtybt/common/PageResult.java
  3. 4 8
      src/main/java/com/yykj/sjnmtybt/controller/AlertController.java
  4. 3 2
      src/main/java/com/yykj/sjnmtybt/controller/DashboardController.java
  5. 14 3
      src/main/java/com/yykj/sjnmtybt/controller/InsuranceMatchController.java
  6. 34 2
      src/main/java/com/yykj/sjnmtybt/controller/PersonnelController.java
  7. 2 1
      src/main/java/com/yykj/sjnmtybt/controller/SpecialBizController.java
  8. 3 3
      src/main/java/com/yykj/sjnmtybt/dms/DmsClient.java
  9. 6 1
      src/main/java/com/yykj/sjnmtybt/dto/request/AmountStandardSaveRequest.java
  10. 2 0
      src/main/java/com/yykj/sjnmtybt/dto/request/BatchQueryRequest.java
  11. 1 0
      src/main/java/com/yykj/sjnmtybt/dto/request/PersonnelQueryRequest.java
  12. 11 0
      src/main/java/com/yykj/sjnmtybt/dto/request/ProcessLogQueryRequest.java
  13. 28 0
      src/main/java/com/yykj/sjnmtybt/dto/request/QualifyReviewBatchRequest.java
  14. 21 0
      src/main/java/com/yykj/sjnmtybt/dto/request/QualifySubmitBatchRequest.java
  15. 2 0
      src/main/java/com/yykj/sjnmtybt/dto/response/PersonnelVO.java
  16. 3 2
      src/main/java/com/yykj/sjnmtybt/dto/response/WorkbenchVO.java
  17. 8 2
      src/main/java/com/yykj/sjnmtybt/service/AlertLifecycleSupport.java
  18. 705 89
      src/main/java/com/yykj/sjnmtybt/service/BizDataService.java
  19. 574 165
      src/main/java/com/yykj/sjnmtybt/service/BizFlowService.java
  20. 4 2
      src/main/java/com/yykj/sjnmtybt/service/InsuranceMatchExportService.java
  21. 210 17
      src/main/java/com/yykj/sjnmtybt/service/PersonnelImportExportService.java

+ 69 - 0
src/main/java/com/yykj/sjnmtybt/common/FlexibleEpochDeserializer.java

@@ -0,0 +1,69 @@
+package com.yykj.sjnmtybt.common;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import org.springframework.util.StringUtils;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 生效时间:兼容毫秒时间戳、秒时间戳、yyyy-MM-dd、yyyy-MM-dd HH:mm:ss。
+ */
+public class FlexibleEpochDeserializer extends JsonDeserializer<Long> {
+
+    private static final DateTimeFormatter YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final DateTimeFormatter YMD_HMS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Override
+    public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        JsonToken token = p.currentToken();
+        if (token == JsonToken.VALUE_NUMBER_INT) {
+            return normalizeEpoch(p.getLongValue());
+        }
+        if (token == JsonToken.VALUE_NUMBER_FLOAT) {
+            return normalizeEpoch((long) p.getDoubleValue());
+        }
+        if (token == JsonToken.VALUE_NULL) {
+            return null;
+        }
+        String raw = p.getValueAsString();
+        if (!StringUtils.hasText(raw)) {
+            return null;
+        }
+        String s = raw.trim().replace('/', '-');
+        if (s.matches("\\d+")) {
+            try {
+                return normalizeEpoch(Long.parseLong(s));
+            } catch (NumberFormatException e) {
+                throw ctxt.weirdStringException(raw, Long.class, "生效时间数字无法解析");
+            }
+        }
+        try {
+            if (s.length() >= 19 && s.charAt(10) == ' ') {
+                return LocalDateTime.parse(s.substring(0, 19), YMD_HMS)
+                        .atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+            }
+            if (s.length() >= 10) {
+                return LocalDate.parse(s.substring(0, 10), YMD)
+                        .atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+            }
+        } catch (Exception e) {
+            throw ctxt.weirdStringException(raw, Long.class, "生效时间须为时间戳或 yyyy-MM-dd");
+        }
+        throw ctxt.weirdStringException(raw, Long.class, "生效时间须为时间戳或 yyyy-MM-dd");
+    }
+
+    private static long normalizeEpoch(long n) {
+        // 10 位按秒
+        if (n > 0 && n < 20_000_000_000L) {
+            return n * 1000L;
+        }
+        return n;
+    }
+}

+ 4 - 0
src/main/java/com/yykj/sjnmtybt/common/PageResult.java

@@ -5,6 +5,7 @@ import lombok.Data;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 @Data
 @Schema(description = "分页结果")
@@ -22,6 +23,9 @@ public class PageResult<T> {
     @Schema(description = "数据列表")
     private List<T> list;
 
+    @Schema(description = "当前筛选全量合计(发放明细等回填,不是当前页小计)")
+    private Map<String, Object> summary;
+
     public static <T> PageResult<T> of(long pageNum, long pageSize, long total, List<T> list) {
         PageResult<T> p = new PageResult<T>();
         p.setPageNum(pageNum);

+ 4 - 8
src/main/java/com/yykj/sjnmtybt/controller/AlertController.java

@@ -37,15 +37,11 @@ public class AlertController {
     @GetMapping("/{id}")
     public ApiResponse<AlertVO> detail(
             @Parameter(description = "告警ID", required = true) @PathVariable String id) {
-        AlertQueryRequest q = new AlertQueryRequest();
-        q.setPageNum(1);
-        q.setPageSize(200);
-        for (AlertVO vo : biz.pageAlerts(q).getList()) {
-            if (id.equals(vo.getId())) {
-                return ApiResponse.ok(vo);
-            }
+        AlertVO vo = biz.getAlert(id);
+        if (vo == null) {
+            return ApiResponse.fail(404, "告警不存在: " + id);
         }
-        return ApiResponse.fail(404, "告警不存在: " + id);
+        return ApiResponse.ok(vo);
     }
 
     @Operation(summary = "处置告警",

+ 3 - 2
src/main/java/com/yykj/sjnmtybt/controller/DashboardController.java

@@ -56,8 +56,9 @@ public class DashboardController {
             @Parameter(description = "街镇ID") @RequestParam(required = false) String townId,
             @Parameter(description = "村居ID") @RequestParam(required = false) String villageId,
             @Parameter(description = "历史月数,默认6") @RequestParam(required = false, defaultValue = "6") Integer months,
-            @Parameter(description = "操作人,用于待办已读过滤") @RequestParam(required = false) String operator) {
-        return ApiResponse.ok(flow.workbench(payMonth, townId, villageId, months, operator));
+            @Parameter(description = "操作人,用于待办已读过滤") @RequestParam(required = false) String operator,
+            @Parameter(description = "角色 key:village/town/social/leader/agri/hr/admin") @RequestParam(required = false) String roleKey) {
+        return ApiResponse.ok(flow.workbench(payMonth, townId, villageId, months, operator, roleKey));
     }
 
     @Operation(summary = "标记工作台待办已读")

+ 14 - 3
src/main/java/com/yykj/sjnmtybt/controller/InsuranceMatchController.java

@@ -1,6 +1,7 @@
 package com.yykj.sjnmtybt.controller;
 
 import com.yykj.sjnmtybt.common.ApiResponse;
+import com.yykj.sjnmtybt.common.PageResult;
 import com.yykj.sjnmtybt.dto.request.InsuranceManualConfirmRequest;
 import com.yykj.sjnmtybt.dto.request.InsuranceMatchTriggerRequest;
 import com.yykj.sjnmtybt.dto.response.InsuranceMatchVO;
@@ -44,10 +45,20 @@ public class InsuranceMatchController {
     @Operation(summary = "查询批次匹配结果列表",
             description = "返回批次内每人居保匹配结果:成功/丧失资格/无法判定等。")
     @GetMapping("/batches/{batchId}/results")
-    public ApiResponse<List<InsuranceMatchVO>> results(
+    public ApiResponse<?> results(
             @Parameter(description = "批次ID", required = true) @PathVariable String batchId,
-            @Parameter(description = "街镇ID,切本镇人员") @RequestParam(required = false) String townId) {
-        return ApiResponse.ok(flow.matchResults(batchId, null, townId));
+            @Parameter(description = "街镇ID,切本镇人员") @RequestParam(required = false) String townId,
+            @Parameter(description = "页码") @RequestParam(required = false) Integer pageNum,
+            @Parameter(description = "每页条数") @RequestParam(required = false) Integer pageSize) {
+        List<InsuranceMatchVO> list = flow.matchResults(batchId, null, townId);
+        if (pageNum == null || pageSize == null) {
+            return ApiResponse.ok(list);
+        }
+        int pn = pageNum < 1 ? 1 : pageNum;
+        int ps = pageSize < 1 ? 10 : pageSize;
+        int from = Math.min((pn - 1) * ps, list.size());
+        int to = Math.min(from + ps, list.size());
+        return ApiResponse.ok(PageResult.of(pn, ps, list.size(), list.subList(from, to)));
     }
 
     @Operation(summary = "人工确认匹配结果",

+ 34 - 2
src/main/java/com/yykj/sjnmtybt/controller/PersonnelController.java

@@ -7,7 +7,9 @@ import com.yykj.sjnmtybt.dto.request.PersonnelInitStatusRequest;
 import com.yykj.sjnmtybt.dto.request.PersonnelQueryRequest;
 import com.yykj.sjnmtybt.dto.request.PersonnelSaveRequest;
 import com.yykj.sjnmtybt.dto.request.PersonnelStatusUpdateRequest;
+import com.yykj.sjnmtybt.dto.request.QualifyReviewBatchRequest;
 import com.yykj.sjnmtybt.dto.request.QualifyReviewRequest;
+import com.yykj.sjnmtybt.dto.request.QualifySubmitBatchRequest;
 import com.yykj.sjnmtybt.dto.request.QualifySubmitRequest;
 import com.yykj.sjnmtybt.dto.response.PersonnelVO;
 import com.yykj.sjnmtybt.service.BizDataService;
@@ -47,7 +49,9 @@ public class PersonnelController {
             description = "支持姓名/身份证/街镇村居/业务状态等筛选;身份证支持模糊。")
     @PostMapping("/page")
     public ApiResponse<PageResult<PersonnelVO>> page(@RequestBody PersonnelQueryRequest req) {
-        return ApiResponse.ok(biz.pagePersonnel(req));
+        PageResult<PersonnelVO> page = biz.pagePersonnel(req);
+        flow.healClosedQualifyRejectAlerts(page.getList());
+        return ApiResponse.ok(page);
     }
 
     @Operation(summary = "下载人员导入模板",
@@ -117,6 +121,7 @@ public class PersonnelController {
         if (vo == null) {
             return ApiResponse.fail(404, "人员不存在: " + id);
         }
+        flow.healClosedQualifyRejectAlerts(java.util.Collections.singletonList(vo));
         return ApiResponse.ok(vo);
     }
 
@@ -129,7 +134,7 @@ public class PersonnelController {
     }
 
     @Operation(summary = "批量删除人员(管理员)",
-            description = "按人员业务号逐人归档并摘离发放明细;单次最多 200 人。")
+            description = "一次加载人员与发放明细后并行归档并摘离;单次最多 2000 人。")
     @PostMapping("/batch-delete")
     public ApiResponse<java.util.Map<String, Object>> batchDelete(@RequestBody java.util.Map<String, Object> body) {
         java.util.List<String> ids = new java.util.ArrayList<String>();
@@ -145,6 +150,15 @@ public class PersonnelController {
         return ApiResponse.ok(String.valueOf(r.get("message")), r);
     }
 
+    @Operation(summary = "清空全部人员底数(管理员)",
+            description = "销毁栏目中全部人员底数并摘离发放明细。须 confirm=CLEAR_ALL_PERSONNEL。")
+    @PostMapping("/clear-all")
+    public ApiResponse<java.util.Map<String, Object>> clearAll(@RequestBody java.util.Map<String, Object> body) {
+        String confirm = body == null || body.get("confirm") == null ? "" : String.valueOf(body.get("confirm"));
+        java.util.Map<String, Object> r = flow.clearAllPersonnel(confirm);
+        return ApiResponse.ok(String.valueOf(r.get("message")), r);
+    }
+
     @Operation(summary = "一键初始化人员业务状态、居保匹配与退养资格(管理员)",
             description = "按街镇/村居或全区改写业务状态、居保匹配、退养资格、本月发放;跳过已归档。"
                     + "不改姓名/身份证/银行卡。资格空或 UNSET 表示清空待认定。须 confirm=INIT_STATUS_CONFIRM。")
@@ -171,6 +185,15 @@ public class PersonnelController {
         return ApiResponse.ok("已提交退养资格审批", flow.submitRetireQualify(id, req));
     }
 
+    @Operation(summary = "村合作社批量提交退养资格审批",
+            description = "一次传入人员ID列表,并行回写,最后只清一次缓存。")
+    @PostMapping("/qualify/submit-batch")
+    public ApiResponse<Map<String, Object>> submitQualifyBatch(
+            @Validated @RequestBody QualifySubmitBatchRequest req) {
+        Map<String, Object> r = flow.submitRetireQualifyBatch(req);
+        return ApiResponse.ok(String.valueOf(r.get("message")), r);
+    }
+
     @Operation(summary = "镇经发中心审批退养资格",
             description = "通过须选择有土地/无土地有资格,人员变为「新增」等下月 1 号纳入;不通过可无资格或驳回补材料。")
     @PostMapping("/{id}/qualify/review")
@@ -184,6 +207,15 @@ public class PersonnelController {
         return ApiResponse.ok("资格审批已驳回", vo);
     }
 
+    @Operation(summary = "镇经发中心批量审批退养资格",
+            description = "一次提交多人,并行回写人员与流程,最后只清一次缓存。")
+    @PostMapping("/qualify/review-batch")
+    public ApiResponse<Map<String, Object>> reviewQualifyBatch(
+            @Validated @RequestBody QualifyReviewBatchRequest req) {
+        Map<String, Object> r = flow.reviewRetireQualifyBatch(req);
+        return ApiResponse.ok(String.valueOf(r.get("message")), r);
+    }
+
     private ResponseEntity<byte[]> csvResponse(byte[] body, String filename) {
         return fileResponse(body, filename, new MediaType("text", "csv", StandardCharsets.UTF_8));
     }

+ 2 - 1
src/main/java/com/yykj/sjnmtybt/controller/SpecialBizController.java

@@ -35,9 +35,10 @@ public class SpecialBizController {
             @Parameter(description = "关联统一批次号") @RequestParam(required = false) String batchNo,
             @Parameter(description = "业务类型") @RequestParam(required = false) SpecialBizTypeEnum bizType,
             @Parameter(description = "审批状态:待分管领导审批/已通过/已驳回") @RequestParam(required = false) String approveStatus,
+            @Parameter(description = "街镇ID") @RequestParam(required = false) String townId,
             @Parameter(description = "页码,从 1 起") @RequestParam(required = false, defaultValue = "1") Integer pageNum,
             @Parameter(description = "每页条数") @RequestParam(required = false, defaultValue = "20") Integer pageSize) {
-        return ApiResponse.ok(flow.pageSpecial(bizType, approveStatus, pageNum, pageSize));
+        return ApiResponse.ok(flow.pageSpecial(bizType, approveStatus, pageNum, pageSize, townId));
     }
 
     @Operation(summary = "恢复发放补发预览",

+ 3 - 3
src/main/java/com/yykj/sjnmtybt/dms/DmsClient.java

@@ -97,7 +97,7 @@ public class DmsClient {
     public List<Map<String, Object>> selectContentListParallel(
             Integer columnId, Integer modelId, int pageSize, int parallelism) {
         int size = pageSize <= 0 ? 50 : Math.min(pageSize, 100);
-        int workers = parallelism <= 1 ? 1 : Math.min(parallelism, 4);
+        int workers = parallelism <= 1 ? 1 : Math.min(parallelism, 8);
         List<Map<String, Object>> all = new ArrayList<Map<String, Object>>();
         // 先取 token,避免并行时重复登录
         getToken(false);
@@ -112,7 +112,7 @@ public class DmsClient {
         if (workers <= 1) {
             int page = 1;
             int guard = 0;
-            while (guard++ < 49) {
+            while (guard++ < 800) {
                 PageSlice slice = selectPage(columnId, modelId, page, size, false);
                 if (slice.rows.isEmpty()) {
                     break;
@@ -129,7 +129,7 @@ public class DmsClient {
         int guard = 0;
         ExecutorService pool = Executors.newFixedThreadPool(workers);
         try {
-            while (guard++ < 25) {
+            while (guard++ < 200) {
                 List<Callable<PageSlice>> tasks = new ArrayList<Callable<PageSlice>>();
                 final int base = nextPage;
                 for (int i = 0; i < workers; i++) {

+ 6 - 1
src/main/java/com/yykj/sjnmtybt/dto/request/AmountStandardSaveRequest.java

@@ -1,5 +1,7 @@
 package com.yykj.sjnmtybt.dto.request;
 
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.yykj.sjnmtybt.common.FlexibleEpochDeserializer;
 import com.yykj.sjnmtybt.enums.StandardTypeEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
@@ -21,7 +23,10 @@ public class AmountStandardSaveRequest {
     private BigDecimal amount;
     @Schema(description = "丧葬金额;YEARLY_ADJUST 必填", example = "6000.00")
     private BigDecimal funeralAmount;
-    @NotNull(message = "生效时间不能为空") @Schema(description = "生效时间毫秒时间戳", required = true) private Long effectiveTime;
+    @NotNull(message = "生效时间不能为空")
+    @JsonDeserialize(using = FlexibleEpochDeserializer.class)
+    @Schema(description = "生效时间:毫秒时间戳或 yyyy-MM-dd", required = true)
+    private Long effectiveTime;
     @Schema(description = "调整原因") private String adjustReason;
     @Schema(description = "备注") private String remarks;
     @Schema(description = "管理员运维直改:跳过分管领导审批直接生效") private Boolean allowDirect;

+ 2 - 0
src/main/java/com/yykj/sjnmtybt/dto/request/BatchQueryRequest.java

@@ -19,4 +19,6 @@ public class BatchQueryRequest extends PageQuery {
     @Schema(description = "批次层级") private BatchLevelEnum batchLevel;
     @Schema(description = "流程阶段") private FlowStageEnum currentStage;
     @Schema(description = "批次状态") private BatchStatusEnum status;
+    @Schema(description = "多个批次状态,逗号分隔,如 EXPORTED,RETURNED,ARCHIVED")
+    private String statusIn;
 }

+ 1 - 0
src/main/java/com/yykj/sjnmtybt/dto/request/PersonnelQueryRequest.java

@@ -21,6 +21,7 @@ public class PersonnelQueryRequest extends PageQuery {
     @Schema(description = "居保匹配结果") private InsuranceMatchResultEnum insuranceMatchResult;
     @Schema(description = "录入来源(模糊包含)") private String source;
     @Schema(description = "退养资格类型") private RetireQualifyTypeEnum retireQualifyType;
+    @Schema(description = "仅查退养资格未认定(无 WITH_LAND/WITHOUT_LAND/INELIGIBLE)") private Boolean retireQualifyUnset;
     @Schema(description = "发放年月,用于当月发放库筛选", example = "202510") private String payMonth;
     @Schema(description = "轻量模式:不回填街镇月补标准(列表/看板加速)") private Boolean lite;
 }

+ 11 - 0
src/main/java/com/yykj/sjnmtybt/dto/request/ProcessLogQueryRequest.java

@@ -14,4 +14,15 @@ public class ProcessLogQueryRequest extends PageQuery {
     @Schema(description = "统一批次号") private String batchNo;
     @Schema(description = "流程阶段") private FlowStageEnum stage;
     @Schema(description = "人员ID") private String personnelId;
+    @Schema(description = "街镇ID") private String townId;
+    @Schema(description = "村居ID") private String villageId;
+    @Schema(description = "业务类型") private String bizType;
+    @Schema(description = "排除的业务类型,逗号分隔,如 SYS_USER,SYS_CONFIG,LOGIN_LOG")
+    private String excludeBizType;
+    @Schema(description = "动作") private String action;
+    @Schema(description = "操作人(模糊)") private String operator;
+    @Schema(description = "操作角色") private String operatorRole;
+    @Schema(description = "处理结果") private String result;
+    @Schema(description = "操作时间起(毫秒)") private Long operateTimeFrom;
+    @Schema(description = "操作时间止(毫秒)") private Long operateTimeTo;
 }

+ 28 - 0
src/main/java/com/yykj/sjnmtybt/dto/request/QualifyReviewBatchRequest.java

@@ -0,0 +1,28 @@
+package com.yykj.sjnmtybt.dto.request;
+
+import com.yykj.sjnmtybt.enums.RetireQualifyTypeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+@Schema(description = "镇经发中心批量退养资格审批")
+public class QualifyReviewBatchRequest {
+    @NotEmpty
+    @Schema(description = "人员业务ID列表", required = true)
+    private List<String> personnelIds;
+    @NotNull
+    @Schema(description = "是否通过", required = true)
+    private Boolean pass;
+    @Schema(description = "通过时必填:WITH_LAND / WITHOUT_LAND;不通过可填 INELIGIBLE")
+    private RetireQualifyTypeEnum retireQualifyType;
+    @Schema(description = "审批意见")
+    private String opinion;
+    @Schema(description = "附件,逗号分隔 URL")
+    private String fileUrls;
+    @Schema(description = "操作人")
+    private String operator;
+}

+ 21 - 0
src/main/java/com/yykj/sjnmtybt/dto/request/QualifySubmitBatchRequest.java

@@ -0,0 +1,21 @@
+package com.yykj.sjnmtybt.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+@Data
+@Schema(description = "村合作社批量提交退养资格审批")
+public class QualifySubmitBatchRequest {
+    @NotEmpty
+    @Schema(description = "人员业务ID列表", required = true)
+    private List<String> personnelIds;
+    @Schema(description = "说明/材料备注")
+    private String opinion;
+    @Schema(description = "附件,逗号分隔 URL")
+    private String fileUrls;
+    @Schema(description = "操作人")
+    private String operator;
+}

+ 2 - 0
src/main/java/com/yykj/sjnmtybt/dto/response/PersonnelVO.java

@@ -20,7 +20,9 @@ public class PersonnelVO {
     @Schema(description = "户籍性质:农业/非农") private String householdType;
     @Schema(description = "区ID") private String districtId;
     @Schema(description = "街镇ID") private String townId;
+    @Schema(description = "街镇名称(查询回填,便于列表直接展示中文)") private String townName;
     @Schema(description = "村居ID") private String villageId;
+    @Schema(description = "村居名称(查询回填,便于列表直接展示中文)") private String villageName;
     @Schema(description = "村委代码") private String villageCode;
     @Schema(description = "委托方案代码") private String entrustPlanCode;
     @Schema(description = "委托方案名称") private String entrustPlanName;

+ 3 - 2
src/main/java/com/yykj/sjnmtybt/dto/response/WorkbenchVO.java

@@ -30,8 +30,8 @@ public class WorkbenchVO {
     @Schema(description = "按街镇资格汇总") private List<TownQualifyRow> townQualify = new ArrayList<TownQualifyRow>();
     @Schema(description = "本月各村人数/金额(镇级)") private List<VillageChartRow> villageCharts = new ArrayList<VillageChartRow>();
     @Schema(description = "本月批次状态计数") private Map<String, Integer> batchStatusCounts = new HashMap<String, Integer>();
-    @Schema(description = "近期未关闭告警(最多8条)") private List<AlertVO> recentAlerts = new ArrayList<AlertVO>();
-    @Schema(description = "本月村批轻量列表(最多50)") private List<BatchVO> monthBatches = new ArrayList<BatchVO>();
+    @Schema(description = "未关闭告警全量") private List<AlertVO> recentAlerts = new ArrayList<AlertVO>();
+    @Schema(description = "本月批次全量") private List<BatchVO> monthBatches = new ArrayList<BatchVO>();
     @Schema(description = "角色待办(含跳转 path)") private List<WorkbenchTodoVO> todos = new ArrayList<WorkbenchTodoVO>();
 
     @Data
@@ -42,6 +42,7 @@ public class WorkbenchVO {
         private Integer withLandCount;
         private Integer withoutLandCount;
         private Integer qualifiedCount;
+        private java.math.BigDecimal amount;
     }
 
     @Data

+ 8 - 2
src/main/java/com/yykj/sjnmtybt/service/AlertLifecycleSupport.java

@@ -112,7 +112,7 @@ public final class AlertLifecycleSupport {
         Long created = vo.getCreateTime() == null ? System.currentTimeMillis() : vo.getCreateTime();
         synthesized.add(event("CREATE", AlertHandleStatusEnum.PENDING.name(),
                 vo.getInitiator() == null ? "系统" : vo.getInitiator(),
-                vo.getContent(), null, created));
+                vo.getContent(), null, vo.getFileUrls(), created));
         if (vo.getHandleStatus() == AlertHandleStatusEnum.PROCESSING
                 || (vo.getHandleStatus() == AlertHandleStatusEnum.CLOSED
                 && StringUtils.hasText(vo.getHandler()))) {
@@ -127,7 +127,7 @@ public final class AlertLifecycleSupport {
             synthesized.add(event("CLOSE", AlertHandleStatusEnum.CLOSED.name(),
                     vo.getHandler() == null ? "系统" : vo.getHandler(),
                     vo.getHandleResult() == null ? "已关闭" : vo.getHandleResult(),
-                    null, t));
+                    null, vo.getFileUrls(), t));
         }
         fillSummary(vo, synthesized);
         vo.setLifecycle(synthesized);
@@ -140,6 +140,10 @@ public final class AlertLifecycleSupport {
             if ("CREATE".equals(it.getAction())) {
                 if (vo.getCreateTime() == null) vo.setCreateTime(it.getOperateTime());
                 if (!StringUtils.hasText(vo.getInitiator())) vo.setInitiator(it.getOperator());
+                if (StringUtils.hasText(vo.getFileUrls()) && !StringUtils.hasText(it.getFileUrls())
+                        && logs.size() == 1) {
+                    it.setFileUrls(vo.getFileUrls());
+                }
             }
             if ("ACCEPT".equals(it.getAction()) || "PROCESS".equals(it.getAction())) {
                 vo.setAcceptedBy(it.getOperator());
@@ -150,6 +154,8 @@ public final class AlertLifecycleSupport {
                 vo.setClosedTime(it.getOperateTime());
                 if (StringUtils.hasText(it.getFileUrls()) && !StringUtils.hasText(vo.getFileUrls())) {
                     vo.setFileUrls(it.getFileUrls());
+                } else if (StringUtils.hasText(vo.getFileUrls()) && !StringUtils.hasText(it.getFileUrls())) {
+                    it.setFileUrls(vo.getFileUrls());
                 }
             }
         }

File diff suppressed because it is too large
+ 705 - 89
src/main/java/com/yykj/sjnmtybt/service/BizDataService.java


File diff suppressed because it is too large
+ 574 - 165
src/main/java/com/yykj/sjnmtybt/service/BizFlowService.java


+ 4 - 2
src/main/java/com/yykj/sjnmtybt/service/InsuranceMatchExportService.java

@@ -61,7 +61,7 @@ public class InsuranceMatchExportService {
                         b.getTownId() == null ? "" : b.getTownId());
                 String villageName = regionNames.getOrDefault(b.getVillageId(),
                         b.getVillageId() == null ? "" : b.getVillageId());
-                List<InsuranceMatchVO> list = flow.matchResults(b.getId(), null);
+                List<InsuranceMatchVO> list = flow.matchResults(b.getId(), null, townId);
                 if (list == null) continue;
                 for (InsuranceMatchVO vo : list) {
                     if (vo == null) continue;
@@ -106,7 +106,9 @@ public class InsuranceMatchExportService {
             if (b == null) {
                 throw new BusinessException(404, "批次不存在: " + batchId);
             }
-            if (StringUtils.hasText(townId) && !townId.equals(b.getTownId())) {
+            if (StringUtils.hasText(townId)
+                    && StringUtils.hasText(b.getTownId())
+                    && !townId.equals(b.getTownId())) {
                 throw new BusinessException(400, "批次不属于指定街镇");
             }
             batches.add(b);

+ 210 - 17
src/main/java/com/yykj/sjnmtybt/service/PersonnelImportExportService.java

@@ -12,6 +12,7 @@ import com.yykj.sjnmtybt.dto.request.PersonnelQueryRequest;
 import com.yykj.sjnmtybt.dto.request.PersonnelSaveRequest;
 import com.yykj.sjnmtybt.dto.response.PersonnelVO;
 import com.yykj.sjnmtybt.dto.response.RegionVO;
+import com.yykj.sjnmtybt.enums.InsuranceMatchResultEnum;
 import com.yykj.sjnmtybt.enums.PersonnelStatusEnum;
 import com.yykj.sjnmtybt.enums.RegionLevelEnum;
 import org.apache.poi.ss.usermodel.Cell;
@@ -39,9 +40,16 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * 人员底数 Excel/CSV 模板、导入、导出。
@@ -288,6 +296,12 @@ public class PersonnelImportExportService {
         List<Map<String, Object>> failRows = new ArrayList<Map<String, Object>>();
         Map<String, String> townIdByName = loadTownIdByName();
         Map<String, String> villageIdByKey = loadVillageIdByKey();
+        Map<String, BigDecimal> standardByTown = new HashMap<String, BigDecimal>();
+        int[] maxBiz = new int[]{100};
+        Map<String, Map<String, Object>> existingByIdNumber = loadExistingByIdNumber(maxBiz);
+        AtomicInteger nextBizSeq = new AtomicInteger(maxBiz[0]);
+        Set<String> seenInFile = new HashSet<String>();
+        List<ImportWork> works = new ArrayList<ImportWork>();
 
         int rowNum = 0;
         boolean headerSkipped = false;
@@ -312,9 +326,17 @@ public class PersonnelImportExportService {
 
             try {
                 PersonnelSaveRequest req = toSaveRequest(cols, colMap, defaultTownId, defaultVillageId,
-                        townIdByName, villageIdByKey);
-                flow.savePersonnel(req);
-                success++;
+                        townIdByName, villageIdByKey, existingByIdNumber, standardByTown);
+                String normId = req.getIdNumber().trim().toUpperCase();
+                if (!seenInFile.add(normId)) {
+                    throw new IllegalArgumentException("本文件内身份证号重复");
+                }
+                ImportWork w = new ImportWork();
+                w.rowNum = rowNum;
+                w.cols = cols;
+                w.req = req;
+                w.existing = existingByIdNumber.get(normId);
+                works.add(w);
             } catch (Exception ex) {
                 fail++;
                 String errMsg = ex.getMessage() == null ? "导入失败" : ex.getMessage();
@@ -323,6 +345,10 @@ public class PersonnelImportExportService {
             }
         }
 
+        int[] writeResult = persistImportWorks(works, nextBizSeq, errors, failRows);
+        success += writeResult[0];
+        fail += writeResult[1];
+
         String fileName = file.getOriginalFilename();
         String errorSummary = buildErrorSummary(errors);
         recordImportHistory(fileName, defaultTownId, defaultVillageId, success, fail, errorSummary);
@@ -346,6 +372,171 @@ public class PersonnelImportExportService {
         return importFile(file, townId, villageId);
     }
 
+    private static final class ImportWork {
+        int rowNum;
+        List<String> cols;
+        PersonnelSaveRequest req;
+        Map<String, Object> existing;
+    }
+
+    /** 一次拉取现有人员,按身份证号索引,避免每行全表查询。 */
+    private Map<String, Map<String, Object>> loadExistingByIdNumber(int[] maxBizHolder) {
+        Map<String, Map<String, Object>> m = new HashMap<String, Map<String, Object>>();
+        if (!dms.isEnabled()) return m;
+        SjnmtybtProperties.Ref ref = props.getDms().getColumns().getPersonnel();
+        List<Map<String, Object>> rows = dms.selectContentListParallel(
+                ref.getColumnId(), ref.getModelId(), 100, 4);
+        if (rows == null) return m;
+        int max = maxBizHolder == null || maxBizHolder.length == 0 ? 100 : maxBizHolder[0];
+        for (Map<String, Object> row : rows) {
+            if (row == null) continue;
+            String biz = DmsRowUtils.personnelBizId(row);
+            if (biz != null && biz.matches("P\\d+")) {
+                try {
+                    max = Math.max(max, Integer.parseInt(biz.substring(1)));
+                } catch (Exception ignore) {
+                }
+            }
+            String idn = DmsRowUtils.str(row, "c_id_number");
+            if (!StringUtils.hasText(idn)) continue;
+            String key = idn.trim().toUpperCase();
+            if (!m.containsKey(key)) m.put(key, row);
+        }
+        if (maxBizHolder != null && maxBizHolder.length > 0) maxBizHolder[0] = max;
+        return m;
+    }
+
+    /** 并行写入 DMS,最后由调用方统一清缓存。返回 [success, fail] */
+    private int[] persistImportWorks(List<ImportWork> works, AtomicInteger nextBizSeq,
+                                     List<String> errors, List<Map<String, Object>> failRows) {
+        if (works == null || works.isEmpty()) return new int[]{0, 0};
+        if (!dms.isEnabled()) {
+            int ok = 0;
+            int fail = 0;
+            for (ImportWork w : works) {
+                try {
+                    flow.savePersonnel(w.req);
+                    ok++;
+                } catch (Exception ex) {
+                    fail++;
+                    String errMsg = ex.getMessage() == null ? "导入失败" : ex.getMessage();
+                    errors.add("第" + w.rowNum + "行:" + errMsg);
+                    failRows.add(buildFailRow(w.rowNum, errMsg, w.cols));
+                }
+            }
+            return new int[]{ok, fail};
+        }
+        final SjnmtybtProperties.Ref ref = props.getDms().getColumns().getPersonnel();
+        int poolSize = Math.max(1, Math.min(8, works.size()));
+        ExecutorService pool = Executors.newFixedThreadPool(poolSize);
+        AtomicInteger ok = new AtomicInteger(0);
+        List<Future<?>> futures = new ArrayList<Future<?>>();
+        try {
+            for (final ImportWork w : works) {
+                futures.add(pool.submit(new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            persistOne(ref, w, nextBizSeq);
+                            int n = ok.incrementAndGet();
+                            if (n % 200 == 0 || n == works.size()) {
+                                System.out.println("[personnel-import] persisted " + n + "/" + works.size());
+                            }
+                        } catch (Exception ex) {
+                            String errMsg = ex.getMessage() == null ? "导入失败" : ex.getMessage();
+                            synchronized (errors) {
+                                errors.add("第" + w.rowNum + "行:" + errMsg);
+                                failRows.add(buildFailRow(w.rowNum, errMsg, w.cols));
+                            }
+                        }
+                    }
+                }));
+            }
+            for (Future<?> f : futures) {
+                try {
+                    f.get(180, TimeUnit.SECONDS);
+                } catch (Exception ex) {
+                    // 单条失败已在任务内记录;超时也记一条
+                    if (ex.getMessage() != null && ex.getMessage().contains("Timeout")) {
+                        synchronized (errors) {
+                            errors.add("写入超时:" + (ex.getMessage() == null ? "未知" : ex.getMessage()));
+                        }
+                    }
+                }
+            }
+        } finally {
+            pool.shutdownNow();
+        }
+        return new int[]{ok.get(), works.size() - ok.get()};
+    }
+
+    private void persistOne(SjnmtybtProperties.Ref ref, ImportWork w, AtomicInteger nextBizSeq) {
+        PersonnelSaveRequest req = w.req;
+        Map<String, Object> existing = w.existing;
+        if (existing != null) {
+            String dmsId = DmsRowUtils.str(existing, "id");
+            String bizId = DmsRowUtils.personnelBizId(existing);
+            if (!StringUtils.hasText(bizId)) bizId = req.getId();
+            Map<String, Object> content = buildImportContent(req, bizId, existing);
+            dms.updateContent(ref.getColumnId(), ref.getModelId(), dmsId, content);
+            Map<String, Object> merged = new LinkedHashMap<String, Object>(existing);
+            merged.putAll(content);
+            data.upsertPersonnelCache(merged);
+            return;
+        }
+        String bizId = "P" + String.format("%03d", Integer.valueOf(nextBizSeq.incrementAndGet()));
+        Map<String, Object> content = buildImportContent(req, bizId, null);
+        String dmsId = dms.addContent(ref.getColumnId(), ref.getModelId(), content);
+        if (StringUtils.hasText(dmsId)) {
+            content.put("id", dmsId);
+        }
+        data.upsertPersonnelCache(content);
+    }
+
+    private Map<String, Object> buildImportContent(PersonnelSaveRequest req, String bizId,
+                                                   Map<String, Object> existing) {
+        Map<String, Object> content = new LinkedHashMap<String, Object>();
+        content.put("title", req.getName());
+        content.put("c_biz_id", bizId);
+        content.put("c_name", req.getName());
+        content.put("c_id_number", req.getIdNumber());
+        content.put("c_district_id", "D_SJ");
+        content.put("c_town_id", req.getTownId());
+        content.put("c_village_id", req.getVillageId());
+        content.put("c_input_village_id", req.getVillageId());
+        content.put("c_phone_number", req.getPhoneNumber());
+        content.put("c_social_insurance_code", req.getSocialInsuranceCode());
+        content.put("c_bank_card_number", req.getBankCardNumber());
+        content.put("c_bank_name", req.getBankName());
+        content.put("c_payee_name", req.getPayeeName() == null ? req.getName() : req.getPayeeName());
+        content.put("c_enjoy_start_month", req.getEnjoyStartMonth());
+        content.put("c_monthly_standard", req.getMonthlyStandard());
+        content.put("c_heir_name", req.getHeirName());
+        content.put("c_heir_id_number", req.getHeirIdNumber());
+        content.put("c_heir_relation", req.getHeirRelation());
+        content.put("c_heir_bank_card", req.getHeirBankCard());
+        content.put("c_heir_phone", req.getHeirPhone());
+        content.put("c_death_date", req.getDeathDate());
+        content.put("c_source", "Excel导入");
+        content.put("c_remarks", req.getRemarks() == null ? "" : req.getRemarks().trim());
+        content.put("content", "MOCK_SEED_260804");
+        if (existing == null) {
+            content.put("c_biz_status", PersonnelStatusEnum.DRAFT.name());
+            content.put("c_pay_this_month", "false");
+            content.put("c_insurance_match_result", InsuranceMatchResultEnum.PENDING.name());
+        } else {
+            String oldStatus = DmsRowUtils.str(existing, "c_biz_status");
+            content.put("c_biz_status", StringUtils.hasText(oldStatus)
+                    ? oldStatus : PersonnelStatusEnum.DRAFT.name());
+            String oldPay = DmsRowUtils.str(existing, "c_pay_this_month");
+            content.put("c_pay_this_month", StringUtils.hasText(oldPay) ? oldPay : "false");
+            String oldMatch = DmsRowUtils.str(existing, "c_insurance_match_result");
+            content.put("c_insurance_match_result", StringUtils.hasText(oldMatch)
+                    ? oldMatch : InsuranceMatchResultEnum.PENDING.name());
+        }
+        return content;
+    }
+
     /** 将导入失败行导出为 xlsx(含错误原因列) */
     @SuppressWarnings("unchecked")
     public byte[] buildFailExportBytes(List<Map<String, Object>> failRows) {
@@ -571,7 +762,9 @@ public class PersonnelImportExportService {
     private PersonnelSaveRequest toSaveRequest(List<String> cols, Map<String, Integer> colMap,
                                                String defaultTown, String defaultVillage,
                                                Map<String, String> townIdByName,
-                                               Map<String, String> villageIdByKey) {
+                                               Map<String, String> villageIdByKey,
+                                               Map<String, Map<String, Object>> existingByIdNumber,
+                                               Map<String, BigDecimal> standardByTown) {
         boolean named = colMap != null && !colMap.isEmpty();
         // 兼容旧模板:姓名,身份证,街镇ID,村居ID,...(第3列不是手机号)
         boolean legacy = !named && cols.size() <= 12 && !looksLikePhone(get(cols, 2))
@@ -691,21 +884,21 @@ public class PersonnelImportExportService {
 
         String townId = resolveTownId(townRaw, defaultTown, townIdByName);
         String villageId = resolveVillageId(villageRaw, townId, defaultVillage, villageIdByKey);
-        BigDecimal standard = resolveMonthlyStandard(townId);
+        BigDecimal standard = standardByTown.get(townId);
+        if (!standardByTown.containsKey(townId)) {
+            standard = data.currentMonthlyStandard(townId);
+            standardByTown.put(townId, standard);
+        }
+        if (standard == null) {
+            throw new BusinessException(400, "未找到街镇当前有效月补标准,无法导入: " + townId);
+        }
 
         String existId = null;
-        PersonnelQueryRequest q = new PersonnelQueryRequest();
-        q.setPageNum(1);
-        q.setPageSize(5);
-        q.setIdNumber(idNumber);
-        PageResult<PersonnelVO> found = data.pagePersonnel(q);
-        if (found.getList() != null) {
-            for (PersonnelVO p : found.getList()) {
-                if (idNumber.equals(p.getIdNumber())) {
-                    existId = p.getId();
-                    break;
-                }
-            }
+        Map<String, Object> existRow = existingByIdNumber == null
+                ? null : existingByIdNumber.get(idNumber.trim().toUpperCase());
+        if (existRow != null) {
+            existId = DmsRowUtils.personnelBizId(existRow);
+            if (!StringUtils.hasText(existId)) existId = DmsRowUtils.str(existRow, "id");
         }
 
         PersonnelSaveRequest req = new PersonnelSaveRequest();

Some files were not shown because too many files changed in this diff