Forráskód Böngészése

build 新版报告平台公司

mamingxu 3 napja
szülő
commit
778d9e7236

+ 5 - 0
easier-report-api/src/main/java/com/yaoyicloud/constant/enums/ModuleType.java

@@ -11,6 +11,11 @@ import lombok.Getter;
 @Getter
 @AllArgsConstructor
 public enum ModuleType {
+
+    CSO_HEADER("CSO_HEADER", "CSO封面模块"),
+
+    CSO_PROMOTION_SUMMARY("CSO_PROMOTION_SUMMARY", "CSO推广总结模块"),
+
     PLATFORM_COMPANY_BASICINFO("PLATFORM_COMPANY_BASICINFO", "平台公司基本信息"),
     FOUNDATION_BASICINFO("FOUNDATION_BASICINFO", "基金会基本信息"),
     ASSOCIATION_BASICINFO("ASSOCIATION_BASICINFO", "学协会基本信息"),

+ 19 - 0
easier-report-api/src/main/java/com/yaoyicloud/dto/ReportDTO.java

@@ -64,4 +64,23 @@ public class ReportDTO {
         @NotNull(message = "reportType必填")
         private  Long relationId;
     }
+
+    /**
+     * 老版报告数据接受
+     */
+    @Data
+    public static class CsoReport {
+
+        @NotNull(message = "map数据必填")
+        private String data;
+
+        @NotNull(message = "模版必填")
+        private String reportTempFile;
+
+        @NotNull(message = "relationId必填")
+        private String relationId;
+
+        @NotNull(message = "moduleType必填")
+        private ModuleType moduleType;
+    }
 }

+ 1 - 1
easier-report-biz/src/main/java/com/yaoyicloud/config/ReportPathManager.java

@@ -82,4 +82,4 @@ public class ReportPathManager {
     public void clear() {
         sessionReportMap.clear();
     }
-}
+}

+ 31 - 20
easier-report-biz/src/main/java/com/yaoyicloud/controller/CsoReportController.java

@@ -10,6 +10,11 @@ import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 
+import com.yaoyicloud.config.CommonDataCache;
+import com.yaoyicloud.config.ReportPathManager;
+import com.yaoyicloud.entity.ReportGenerationResult;
+import com.yaoyicloud.service.CsoReportService;
+import com.yaoyicloud.service.ReportUpdateService;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -22,6 +27,8 @@ import com.yaoyicloud.service.ReportService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 
+import static com.yaoyicloud.config.SessionInterceptor.SESSION_MAP;
+
 
 /**
  * 报告控制器
@@ -33,43 +40,47 @@ import lombok.extern.slf4j.Slf4j;
 @RequiredArgsConstructor
 @Slf4j
 public class CsoReportController {
-    private final ReportService reportService;
+    private final CsoReportService csoReportService;
+    private final ReportPathManager reportPathManager;
+    private final CommonDataCache commonDataCache;
     /**
      * 创建Plus版本审核报告
      *
      * @param resource 请求参数
      * @return {@link Boolean } 结果
      */
-    @EasierLog("扬子江审核报告")
     @PostMapping("/report/create-report-cso")
     public Map<String, Object>
-    createPlusVersionCheckReport(@Validated @RequestBody ReportDTO.OnCreateVersionReport resource,
+    createPlusVersionCheckReport(@Validated @RequestBody ReportDTO.CsoReport resource,
                                  HttpServletRequest request) throws Exception {
         byte[] fileBytes = Base64.getDecoder().decode(resource.getReportTempFile());
 
-
+        String relationId = request.getHeader("relationId");
         synchronized (this) {
-            String reportPath = reportService.createPlusVersionCheckReport(
-                    resource.getReportType(),
+            ReportGenerationResult plusVersionCheckReport = csoReportService.createCsoCheckReport(
                     resource.getData(),
                     fileBytes,
-                    resource.getReportBastPath(),
-                    resource.getRelationId()
+                    Long.valueOf(resource.getRelationId()),
+                    resource.getModuleType()
             );
-            //把结果保存到session中 一个relationId一个session 这样就把多个结果都保存到一个session中,可以获取路径做拼接
-            //后续 session.removeAttribute清理数据。
-            HttpSession session = request.getSession();
-            String relationId = String.valueOf(resource.getRelationId());
-            @SuppressWarnings("unchecked")
-            List<String> reportPaths = (List<String>) session.getAttribute(relationId);
+            String sessionId = SESSION_MAP.get(relationId);
 
-            if (reportPaths == null) {
-                reportPaths = new ArrayList<>();
-                session.setAttribute(relationId, reportPaths);
-            }
+
+            // 3. 构建响应
             Map<String, Object> response = new HashMap<>();
-            response.put("reportResult", reportPath);
-            response.put("sessionId", request.getSession().getId()); // Use the provided session ID
+            // 根据是否是最后一个模块决定返回哪个路径
+            if (plusVersionCheckReport.isLastModule() && plusVersionCheckReport.getMergedReportPath() != null) {
+                response.put("reportResult", plusVersionCheckReport.getMergedReportPath());
+                response.put("isFinalReport", true);
+                reportPathManager.removeSession(sessionId);
+                commonDataCache.removeSessionData(sessionId);
+
+            } else {
+                response.put("reportResult", plusVersionCheckReport.getModulePath());
+                response.put("isFinalReport", false);
+            }
+
+            response.put("sessionId", sessionId);
             return response;
         }
     }

+ 1 - 0
easier-report-biz/src/main/java/com/yaoyicloud/controller/ReportUpdateController.java

@@ -59,6 +59,7 @@ public class ReportUpdateController {
                 Long.valueOf(relationId),
                 resource.getModuleType()
         );
+
         String sessionId = SESSION_MAP.get(relationId);
 
 

+ 405 - 1
easier-report-biz/src/main/java/com/yaoyicloud/render/AbstractRender.java

@@ -10,9 +10,11 @@ import java.text.DecimalFormat;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -29,6 +31,7 @@ import com.deepoove.poi.data.PictureType;
 import com.deepoove.poi.data.style.PictureStyle;
 import com.deepoove.poi.exception.RenderException;
 import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
+import com.deepoove.poi.policy.DynamicTableRenderPolicy;
 import com.deepoove.poi.policy.ParagraphRenderPolicy;
 import com.deepoove.poi.policy.PictureRenderPolicy;
 import com.deepoove.poi.policy.RenderPolicy;
@@ -39,6 +42,7 @@ import com.deepoove.poi.util.TableTools;
 import com.deepoove.poi.xwpf.BodyContainer;
 import com.deepoove.poi.xwpf.BodyContainerFactory;
 import com.yaoyicloud.tools.Util;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.pdfbox.Loader;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.rendering.PDFRenderer;
@@ -60,6 +64,7 @@ import javax.imageio.ImageIO;
  * 抽象渲染器
  *
  */
+@Slf4j
 public abstract class AbstractRender {
 
     /*
@@ -112,7 +117,6 @@ public abstract class AbstractRender {
                 throw new IOException("无法创建文件保存目录");
             }
         }
-
         Configure config = builder.build();
         XWPFTemplate template =
             XWPFTemplate.compile(new ByteArrayInputStream(templateFileContent), config).render(dataMap);
@@ -151,6 +155,16 @@ public abstract class AbstractRender {
         return this.htmlResultPath;
     }
 
+    // /**
+    // * Docx 渲染
+    // * @param info 数据
+    // * @param templateFileContent 模板内容
+    // * @param relationId 用于查询缓存
+    // * @return
+    // * @throws IOException
+    // */
+    // public abstract String renderDocx(String info, byte[] templateFileContent, String relationId) throws IOException;
+
     /**
      * html转换为pdf
      *
@@ -735,4 +749,394 @@ public abstract class AbstractRender {
             super.render(eleTemplate, data, template);
         }
     }
+
+    /**
+     * 数据渲染策略
+     *
+     * @return {@link DynamicTableRenderPolicy } 渲染策略对象
+     */
+    protected DynamicTableRenderPolicy getTwoLevelTableDynamicPolicy() {
+        return new DynamicTableRenderPolicy() {
+            @SuppressWarnings("unchecked")
+            @Override
+            public void render(XWPFTable table, Object data) {
+                try {
+                    @SuppressWarnings("unchecked")
+                    Map<String, List<List<Object>>> keyedData = (Map<String, List<List<Object>>>) data;
+
+                    // 起始行
+                    int startRow = 1;
+
+                    // Reserve the first and the last row
+                    for (int i = table.getNumberOfRows() - 2; i >= startRow; i--) {
+                        table.removeRow(i);
+                    }
+
+                    // Rows in middle
+                    int baseNameSeq = 0;
+                    for (Map.Entry<String, List<List<Object>>> entry : keyedData.entrySet()) {
+                        baseNameSeq++;
+                        String baseTypeName = entry.getKey();
+                        List<List<Object>> rowList = entry.getValue();
+
+                        // 当前大类
+                        List<Object> summary = null;
+                        for (int i = 0; i < rowList.size(); i++) {
+                            List<Object> values = rowList.get(i);
+
+                            XWPFTableRow addRow = table.insertNewTableRow(startRow);
+                            if (i == 0) {
+                                Util.setText(addRow.addNewTableCell(), Integer.toString(baseNameSeq));
+                                Util.setText(addRow.addNewTableCell(), baseTypeName);
+                            } else {
+                                Util.setText(addRow.addNewTableCell(), "");
+                                Util.setText(addRow.addNewTableCell(), "");
+                            }
+                            for (int j = 0; j < values.size(); j++) {
+                                String text = getText(table, j, values.get(j));
+                                Util.setText(addRow.addNewTableCell(), text);
+                            }
+                            startRow++;
+
+                            // 统计summary
+                            if (summary == null) {
+                                summary = values.stream().collect(Collectors.toList());
+                                continue;
+                            }
+                            List<Object> origin = summary;
+                            summary = new ArrayList<>();
+                            for (int j = 0; j < values.size(); j++) {
+                                Object value = values.get(j);
+                                String head = table.getRow(0).getCell(j + 2).getText();
+                                if (isAvg(head)) {
+                                    // calculate avg by hardcoding previous two columns
+                                    Object divider = summary.get(j - 2) instanceof Set
+                                        ? ((Set) summary.get(j - 2)).size() : origin.get(j - 2);
+                                    summary.add(Long.valueOf(summary.get(j - 1).toString()) * 1d
+                                        / Long.valueOf(divider.toString()));
+                                    continue;
+                                }
+                                if (value instanceof Set) {
+                                    ((Set<String>) origin.get(j)).addAll((Set<String>) value);
+                                    summary.add(origin.get(j));
+                                    continue;
+                                }
+                                if (value instanceof Long) {
+                                    summary.add((Long) (origin.get(j)) + (Long) value);
+                                } else if (value instanceof Integer) {
+                                    summary.add((Integer) (origin.get(j)) + (Integer) value);
+                                } else if (value instanceof Double) {
+                                    summary.add((Double) (origin.get(j)) + (Double) value);
+                                } else {
+                                    summary.add("");
+                                }
+                            }
+                        }
+                        // section summary
+                        if (rowList.size() > 0) {
+                            XWPFTableRow addRow = table.insertNewTableRow(startRow);
+                            Util.setText(addRow.addNewTableCell(), "");
+                            Util.setText(addRow.addNewTableCell(), "");
+                            Util.colorSummary(addRow.addNewTableCell(), "合计");
+                            for (int j = 1; j < summary.size(); j++) {
+                                Util.colorSummary(addRow.addNewTableCell(), getText(table, j, summary.get(j)));
+                            }
+                        }
+                        startRow++;
+                    }
+
+                    // last row
+                    XWPFTableRow lastRow = table.getRow(table.getNumberOfRows() - 1);
+                    int numOfCol = 1;
+                    while (lastRow.getCell(numOfCol) != null) {
+                        numOfCol++;
+                    }
+                    List<List<Object>> values =
+                        keyedData.values().stream().flatMap(List::stream).collect(Collectors.toList());
+                    for (int i = 1; i < numOfCol; i++) {
+                        XWPFTableCell cell = lastRow.getCell(i);
+                        String head = table.getRow(0).getCell(i + 2).getText();
+                        final int idx = i;
+                        if (isPercentage(head)) {
+                            Util.replaceLastRowText(cell, "100.00%");
+                        } else if (values.size() > 0
+                            && (values.get(0).get(i) instanceof Long || values.get(0).get(i) instanceof Integer)) {
+                            Long value = values.stream().mapToLong(row -> Long.valueOf(row.get(idx).toString())).sum();
+                            Util.replaceLastRowText(cell, value.toString());
+                        } else if (values.size() > 0 && (values.get(0).get(i) instanceof Set)) {
+                            Set<String> value = values.stream().map((row) -> (Set<String>) row.get(idx))
+                                .reduce(new HashSet<String>(), (r, st) -> {
+                                    r.addAll(st);
+                                    return r;
+                                });
+                            Util.replaceLastRowText(cell, Integer.toString(value.size()));
+                        }
+                    }
+
+                    // First column is seq number, the second is first category
+                    Util.mergeFirstNCol(table, 2, 1);
+                } catch (Exception e) {
+                    log.error("渲染模板失败", e);
+                    throw e;
+                }
+
+                table.getRows().forEach(row -> {
+                    row.setHeight(1);
+                });
+            }
+
+            @SuppressWarnings("unchecked")
+            private String getText(XWPFTable table, int j, Object value) {
+                String text = value.toString();
+                if (value instanceof Double) {
+                    text = String.format("%.2f", value);
+                }
+                String head = table.getRow(0).getCell(j + 2).getText();
+                if (isPercentage(head)) {
+                    text = String.format("%.2f", (Double) value * 100);
+                    text += "%";
+                }
+                if (value instanceof Set) {
+                    text = Integer.toString(((Set<String>) value).size());
+                }
+                return text;
+            }
+
+            private boolean isPercentage(String head) {
+                return head.contains("占比") || head.contains("率");
+            }
+
+            private boolean isAvg(String head) {
+                return head.contains("均");
+            }
+        };
+    }
+
+    /**
+     * 数据渲染策略
+     *
+     * @return {@link DynamicTableRenderPolicy } 渲染策略对象
+     */
+    protected DynamicTableRenderPolicy getTableLastRowDynamicPolicy() {
+        return new DynamicTableRenderPolicy() {
+            @Override
+            public void render(XWPFTable table, Object data) {
+                try {
+                    @SuppressWarnings("unchecked")
+                    List<Object> rowData = (List<Object>) data;
+                    for (int i = 0; i < rowData.size(); i++) {
+                        Object value = rowData.get(i);
+                        if (value == null) {
+                            continue;
+                        }
+                        XWPFTableCell cell = table.getRow(table.getNumberOfRows() - 1).getCell(i);
+                        Util.replaceLastRowText(cell, value.toString());
+                    }
+                } catch (Exception e) {
+                    log.error("渲染模板失败", e);
+                    throw e;
+                }
+            }
+        };
+    }
+
+    protected DynamicTableRenderPolicy tableDynamicPolicy(Boolean sensitiveFlag, Map<Integer, String> userNameMap) {
+        return new DynamicTableRenderPolicy() {
+            @SuppressWarnings("unchecked")
+            @Override
+            public void render(XWPFTable table, Object data) {
+                try {
+                    @SuppressWarnings("unchecked")
+                    Map<String, List<List<Object>>> keyedData = (Map<String, List<List<Object>>>) data;
+
+                    // 起始行
+                    int startRow = 1;
+
+                    // Reserve the first and the last row
+                    for (int i = table.getNumberOfRows() - 2; i >= startRow; i--) {
+                        table.removeRow(i);
+                    }
+
+                    // Rows in middle
+                    int baseNameSeq = 0;
+                    for (Map.Entry<String, List<List<Object>>> entry : keyedData.entrySet()) {
+                        baseNameSeq++;
+                        String userIdKey = entry.getKey();
+                        String baseTypeName = userNameMap.getOrDefault(Integer.valueOf(userIdKey), "无姓名");
+                        if (sensitiveFlag) {
+                            baseTypeName = chineseName(baseTypeName);
+                        }
+                        List<List<Object>> rowList = entry.getValue();
+
+                        // 当前大类
+                        List<Object> summary = null;
+                        for (int i = 0; i < rowList.size(); i++) {
+                            List<Object> values = rowList.get(i);
+
+                            XWPFTableRow addRow = table.insertNewTableRow(startRow);
+                            if (i == 0) {
+                                Util.setText(addRow.addNewTableCell(), Integer.toString(baseNameSeq));
+                                Util.setText(addRow.addNewTableCell(), baseTypeName);
+                            } else {
+                                Util.setText(addRow.addNewTableCell(), "");
+                                Util.setText(addRow.addNewTableCell(), "");
+                            }
+                            for (int j = 0; j < values.size(); j++) {
+                                String text = getText(table, j, values.get(j));
+                                Util.setText(addRow.addNewTableCell(), text);
+                            }
+                            startRow++;
+
+                            // 统计summary
+                            if (summary == null) {
+                                summary = values.stream().collect(Collectors.toList());
+                                continue;
+                            }
+                            List<Object> origin = summary;
+                            summary = new ArrayList<>();
+                            for (int j = 0; j < values.size(); j++) {
+                                Object value = values.get(j);
+                                String head = table.getRow(0).getCell(j + 2).getText();
+                                if (isAvg(head)) {
+                                    // calculate avg by hardcoding previous two columns
+                                    Object divider = summary.get(j - 2) instanceof Set
+                                        ? ((Set) summary.get(j - 2)).size() : origin.get(j - 2);
+                                    summary.add(Long.valueOf(summary.get(j - 1).toString()) * 1d
+                                        / Long.valueOf(divider.toString()));
+                                    continue;
+                                }
+                                if (value instanceof Set) {
+                                    ((Set<String>) origin.get(j)).addAll((Set<String>) value);
+                                    summary.add(origin.get(j));
+                                    continue;
+                                }
+                                if (value instanceof Long) {
+                                    summary.add((Long) (origin.get(j)) + (Long) value);
+                                } else if (value instanceof Integer) {
+                                    summary.add((Integer) (origin.get(j)) + (Integer) value);
+                                } else if (value instanceof Double) {
+                                    summary.add((Double) (origin.get(j)) + (Double) value);
+                                } else {
+                                    summary.add("");
+                                }
+                            }
+                        }
+                        // section summary
+                        if (rowList.size() > 0) {
+                            XWPFTableRow addRow = table.insertNewTableRow(startRow);
+                            Util.setText(addRow.addNewTableCell(), "");
+                            Util.setText(addRow.addNewTableCell(), "");
+                            Util.colorSummary(addRow.addNewTableCell(), "合计");
+                            for (int j = 1; j < summary.size(); j++) {
+                                Util.colorSummary(addRow.addNewTableCell(), getText(table, j, summary.get(j)));
+                            }
+                        }
+                        startRow++;
+                    }
+
+                    // last row
+                    XWPFTableRow lastRow = table.getRow(table.getNumberOfRows() - 1);
+                    int numOfCol = 1;
+                    while (lastRow.getCell(numOfCol) != null) {
+                        numOfCol++;
+                    }
+                    List<List<Object>> values =
+                        keyedData.values().stream().flatMap(List::stream).collect(Collectors.toList());
+                    for (int i = 1; i < numOfCol; i++) {
+                        XWPFTableCell cell = lastRow.getCell(i);
+                        String head = table.getRow(0).getCell(i + 2).getText();
+                        final int idx = i;
+                        if (isPercentage(head)) {
+                            Util.replaceLastRowText(cell, "100.00%");
+                        } else if (values.size() > 0
+                            && (values.get(0).get(i) instanceof Long || values.get(0).get(i) instanceof Integer)) {
+                            Long value = values.stream().mapToLong(row -> Long.valueOf(row.get(idx).toString())).sum();
+                            Util.replaceLastRowText(cell, value.toString());
+                        } else if (values.size() > 0 && (values.get(0).get(i) instanceof Set)) {
+                            Set<String> value = values.stream().map((row) -> (Set<String>) row.get(idx))
+                                .reduce(new HashSet<String>(), (r, st) -> {
+                                    r.addAll(st);
+                                    return r;
+                                });
+                            Util.replaceLastRowText(cell, Integer.toString(value.size()));
+                        }
+                    }
+
+                    // First column is seq number, the second is first category
+                    Util.mergeFirstNCol(table, 2, 1);
+                } catch (Exception e) {
+                    log.error("渲染模板失败", e);
+                    throw e;
+                }
+
+                table.getRows().forEach(row -> {
+                    row.setHeight(1);
+                });
+            }
+
+            @SuppressWarnings("unchecked")
+            private String getText(XWPFTable table, int j, Object value) {
+                String text = value.toString();
+                if (value instanceof Double) {
+                    text = String.format("%.2f", value);
+                }
+                String head = table.getRow(0).getCell(j + 2).getText();
+                if (isPercentage(head)) {
+                    text = String.format("%.2f", (Double) value * 100);
+                    text += "%";
+                }
+                if (value instanceof Set) {
+                    text = Integer.toString(((Set<String>) value).size());
+                }
+                return text;
+            }
+
+            private boolean isPercentage(String head) {
+                return head.contains("占比") || head.contains("率");
+            }
+
+            private boolean isAvg(String head) {
+                return head.contains("均");
+            }
+        };
+    }
+    /**
+     * 【中文姓名】只显示最后一个汉字,其他隐藏为星号,比如:**梦
+     * @param fullName 姓名
+     * @return 结果
+     */
+    public static String chineseName(String fullName) {
+        if (fullName == null) {
+            return null;
+        }
+        return desValue(fullName, 0, 1, "*");
+    }
+
+    /**
+     * 对字符串进行脱敏操作
+     * @param origin 原始字符串
+     * @param prefixNoMaskLen 左侧需要保留几位明文字段
+     * @param suffixNoMaskLen 右侧需要保留几位明文字段
+     * @param maskStr 用于遮罩的字符串, 如'*'
+     * @return 脱敏后结果
+     */
+    public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {
+        if (origin == null) {
+            return null;
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0, n = origin.length(); i < n; i++) {
+            if (i < prefixNoMaskLen) {
+                sb.append(origin.charAt(i));
+                continue;
+            }
+            if (i > (n - suffixNoMaskLen - 1)) {
+                sb.append(origin.charAt(i));
+                continue;
+            }
+            sb.append(maskStr);
+        }
+        return sb.toString();
+    }
 }

+ 1 - 3
easier-report-biz/src/main/java/com/yaoyicloud/render/ServiceProviderInfoRender.java

@@ -77,19 +77,17 @@ public final class ServiceProviderInfoRender extends AbstractRender {
         data.replaceAll((k, v) -> v.equals("") ? "-" : v);
         String tenantName = (String) data.get("tenantName");
         String name = (String) data.get("serviceProviderName");
-        String type = (String) data.get("type") ;
+        String type = (String) data.get("type");
         String reportDate =  LocalDate.now().toString();
         ConcurrentMap<String, Object> commonData = new ConcurrentHashMap<>();
         commonData.put("tenantName", tenantName);
         commonData.put("type", type);
         commonData.put("reportDate", reportDate);
         commonData.put("name", name);
-
         commonDataCache.addData(relationId, commonData);
         data.put("levelInteger", counter);
         data.putAll(commonData);
         fillDefaultValues(data);
-
         try {
             // 渲染文档
             String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "serviceProviderInfo");

+ 109 - 0
easier-report-biz/src/main/java/com/yaoyicloud/render/cso/EntHeaderSectionRender.java

@@ -0,0 +1,109 @@
+package com.yaoyicloud.render.cso;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import cn.hutool.core.date.DateUtil;
+import com.deepoove.poi.config.Configure;
+import com.deepoove.poi.config.ConfigureBuilder;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.protobuf.util.JsonFormat;
+import com.yaoyicloud.config.CommonDataCache;
+import com.yaoyicloud.config.FilerepoProperties;
+import com.yaoyicloud.message.CSOProtos;
+import com.yaoyicloud.render.AbstractRender;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.MapUtils;
+
+@Slf4j
+public final class EntHeaderSectionRender extends AbstractRender {
+    private final FilerepoProperties filerepoProperties;
+    private final CommonDataCache commonDataCache;
+
+    public EntHeaderSectionRender(String cwd, FilerepoProperties filerepoProperties, CommonDataCache commonDataCache) {
+        super(cwd);
+        this.filerepoProperties = filerepoProperties;
+        this.commonDataCache = commonDataCache;
+    }
+
+    @Override
+    protected String getBasicPath() throws IOException {
+        return filerepoProperties.getBasePath();
+    }
+
+    @Override
+    protected String getReportImagePath() {
+        return filerepoProperties.getReportImagePath();
+    }
+
+    /**
+     *  Docx 渲染
+     * @param info 数据
+     * @param templateFileContent 模板内容
+     * @param relationId  用于查询缓存
+     * @return
+     * @throws IOException
+     */
+    public String renderDocx(String info, byte[] templateFileContent, String relationId) throws IOException {
+        log.info("开始渲染CSO企业报告封面模块,relationId: {}", relationId);
+
+        // 配置POI-TL渲染器
+        ConfigureBuilder builder = Configure.builder();
+        builder.bind("drugDetails", this.indicatorsRenderPolicyToProtobuf());
+        CSOProtos.ReportHeader.Builder header = CSOProtos.ReportHeader.newBuilder();
+
+        JsonFormat.parser().merge(info, header);
+        CSOProtos.ReportHeader defaultInstance = CSOProtos.ReportHeader.getDefaultInstance();
+        CSOProtos.ReportHeader mergedProto = defaultInstance.toBuilder()
+            .mergeFrom(header.build())
+            .build();
+
+        String completeJson = JsonFormat.printer()
+            .includingDefaultValueFields()
+            .print(mergedProto);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        Map<String, Object> data = objectMapper.readValue(completeJson, new TypeReference<Map<String, Object>>() {});
+        ConcurrentMap<String, Object> commonData = new ConcurrentHashMap<>();
+
+        commonData.put("reportCreateTime", DateUtil.format(LocalDateTime.now(), "yyyy年MM月dd日 HH:mm:ss"));
+        commonDataCache.addData(relationId, commonData);
+        Map<String, Object> commonDataCacheData = commonDataCache.getData(relationId);
+        if (MapUtils.isNotEmpty(commonDataCacheData)) {
+            data.putAll(commonDataCacheData);
+        }
+        commonData.put("totalPackageScore", data.get("totalPackageScore"));
+        commonData.put("totalApprovedScore", data.get("totalApprovedScore"));
+        commonData.put("taskUserNames", data.get("taskUserNames"));
+        commonData.put("submitedTaskScore", data.get("submitedTaskScore"));
+        commonData.put("createTime", data.get("createTime"));
+        commonData.put("serviceEndDate", data.get("serviceEndDate"));
+        commonData.put("sensitiveFlag", data.get("sensitiveFlag"));
+        commonData.put("scorePackageName", data.get("scorePackageName"));
+
+        commonDataCache.addData(relationId, commonData);
+        fillDefaultValues(data);
+        try {
+            // 渲染文档
+            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "HeaderSection");
+            log.info("渲染CSO企业报告封面模块成功,文件路径: {}", resultPath);
+            return resultPath;
+        } catch (Exception e) {
+            log.error("渲染CSO企业报告封面模块失败,relationId: {}", relationId, e);
+            throw new IOException("文档渲染失败", e);
+        }
+    }
+
+    /**
+     * 填充默认值,确保所有必要字段都存在
+     */
+    private void fillDefaultValues(Map<String, Object> data) {
+
+    }
+
+}

+ 411 - 0
easier-report-biz/src/main/java/com/yaoyicloud/render/cso/EntPromotionSectionRender.java

@@ -0,0 +1,411 @@
+package com.yaoyicloud.render.cso;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.collections4.MapUtils;
+
+import com.deepoove.poi.config.Configure;
+import com.deepoove.poi.config.ConfigureBuilder;
+import com.deepoove.poi.policy.DynamicTableRenderPolicy;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.primitives.Longs;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.util.JsonFormat;
+import com.yaoyicloud.config.CommonDataCache;
+import com.yaoyicloud.config.FilerepoProperties;
+import com.yaoyicloud.message.CSOProtos;
+import com.yaoyicloud.render.AbstractRender;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public final class EntPromotionSectionRender extends AbstractRender {
+    private final FilerepoProperties filerepoProperties;
+    private final CommonDataCache commonDataCache;
+
+    public EntPromotionSectionRender(String cwd, FilerepoProperties filerepoProperties,
+        CommonDataCache commonDataCache) {
+        super(cwd);
+        this.filerepoProperties = filerepoProperties;
+        this.commonDataCache = commonDataCache;
+    }
+
+    @Override
+    protected String getBasicPath() throws IOException {
+        return filerepoProperties.getBasePath();
+    }
+
+    @Override
+    protected String getReportImagePath() {
+        return filerepoProperties.getReportImagePath();
+    }
+
+    /**
+     * Docx 渲染
+     * 
+     * @param info 数据
+     * @param templateFileContent 模板内容
+     * @param relationId 用于查询缓存
+     * @return
+     * @throws IOException
+     */
+    public String renderDocx(String info, byte[] templateFileContent, String relationId) throws IOException {
+        log.info("开始渲染CSO企业报告推广总结模块,relationId: {}", relationId);
+
+        CSOProtos.PromotionSummary.Builder promotionSummary = CSOProtos.PromotionSummary.newBuilder();
+        JsonFormat.parser().merge(info, promotionSummary);
+
+        // 获取用户提交的积分数据
+        Map<String, ByteString> userSubmittedScoreMapMap = promotionSummary.getUserSubmittedScoreMapMap();
+
+        // 关键步骤:将Protobuf的bytes数据转换为Map<String, List<Integer>>
+        Map<String, List<Integer>> userScoreMap = new HashMap<>();
+        for (Map.Entry<String, ByteString> entry : userSubmittedScoreMapMap.entrySet()) {
+            String userId = entry.getKey();
+            ByteString byteString = entry.getValue();
+            // 解析bytes为ListValue
+            ListValue listValue = ListValue.parseFrom(byteString);
+            // 转换为List<Integer>
+            List<Integer> scoreList = listValue.getValuesList().stream()
+                .map(value -> (int) value.getNumberValue())
+                .collect(Collectors.toList());
+            userScoreMap.put(userId, scoreList);
+        }
+        CSOProtos.PromotionSummary defaultInstance = CSOProtos.PromotionSummary.getDefaultInstance();
+        CSOProtos.PromotionSummary mergedProto = defaultInstance.toBuilder()
+            .mergeFrom(promotionSummary.build())
+            .build();
+
+        String completeJson = JsonFormat.printer()
+            .includingDefaultValueFields()
+            .print(mergedProto);
+
+        ObjectMapper objectMapper = new ObjectMapper();
+        Map<String, Object> data = objectMapper.readValue(completeJson, new TypeReference<Map<String, Object>>() {});
+        // 将用户积分数据添加到渲染数据中
+        data.put("userScoreMap", userScoreMap); // 这将使数据在模板中可用
+        Map<String, Object> commonDataCacheData = commonDataCache.getData(relationId);
+        if (MapUtils.isNotEmpty(commonDataCacheData)) {
+            data.putAll(commonDataCacheData);
+        }
+
+        fillDefaultValues(data);
+
+        boolean sensitiveFlag = (Boolean) data.get("sensitiveFlag");
+        Map<Integer, String> userNameMap = (Map<Integer, String>) data.get("userNameMap");
+        ConfigureBuilder builder = Configure.builder();
+        DynamicTableRenderPolicy twoLevelTableDynamicPolicy = getTwoLevelTableDynamicPolicy();
+        DynamicTableRenderPolicy tableDynamicPolicy = tableDynamicPolicy(sensitiveFlag, userNameMap);
+        DynamicTableRenderPolicy lastRowDaynamicPolicy = getTableLastRowDynamicPolicy();
+        builder.bind("task_analysis_5_1", twoLevelTableDynamicPolicy);
+        builder.bind("task_promotion_count_5_1_1", twoLevelTableDynamicPolicy);
+        builder.bind("task_promotion_count_5_1_1_last", lastRowDaynamicPolicy);
+        builder.bind("task_promotioner_5_4", tableDynamicPolicy);
+        builder.bind("task_promotioner_quality_5_4_1", this.indicatorsRenderPolicyToProtobuf());
+        builder.bind("task_promotioner_quality_5_4_1_last", lastRowDaynamicPolicy);
+        try {
+            // 渲染文档
+            String resultPath = this.renderDocx(data, templateFileContent, builder, relationId, "PromotionSection");
+            log.info("渲染CSO企业报告推广总结模块成功,文件路径: {}", resultPath);
+            return resultPath;
+        } catch (Exception e) {
+            log.error("渲染CSO企业报告推广总结模块失败,relationId: {}", relationId, e);
+            throw new IOException("文档渲染失败", e);
+        }
+    }
+
+    /**
+     * 填充默认值,确保所有必要字段都存在
+     */
+    @SuppressWarnings("checkstyle:MethodLength")
+    private void fillDefaultValues(Map<String, Object> data) {
+
+        Integer submitedTaskScore = (Integer) data.get("submitedTaskScore");
+        Integer totalApprovedScore = (Integer) data.get("totalApprovedScore");
+        data.put("rejectedTaskScore", submitedTaskScore - totalApprovedScore);
+        // 提取任务类型元数据(taskTypeMetas)
+        List<Map<String, Object>> taskTypeMetas = (List<Map<String, Object>>) data.get("taskTypeMetas");
+        // 提取按类型统计的任务数据(taskStatsByType)
+        List<Map<String, Object>> taskStatsByTypeList = (List<Map<String, Object>>) data.get("taskStatsByType");
+        // 提取按用户统计的任务数据(taskStatsByPerson)
+        List<Map<String, Object>> taskStatsByPersonList = (List<Map<String, Object>>) data.get("taskStatsByPerson");
+        // 提取全局统计字段
+        int approvedCnt = (Integer) data.getOrDefault("taskPromotionerQuality541ApprovedCnt", 0);
+        int submittedCnt = (Integer) data.getOrDefault("taskPromotionerQuality541SubmittedCnt", 0);
+        int rejectedCnt = (Integer) data.getOrDefault("taskPromotionerQuality541RejectedCnt", 0);
+
+        Map<String, List<Integer>> userScoreMap = (Map<String, List<Integer>>) data.get("userScoreMap");
+        Long scoreSum = 0L;
+        for (Map.Entry<String, List<Integer>> stringListEntry : userScoreMap.entrySet()) {
+            scoreSum = stringListEntry.getValue().stream().mapToLong(Long::valueOf).sum();
+        }
+
+        Map<String, String> userNameMap = taskTypeMetas.stream()
+            .collect(Collectors.toMap(
+                m -> m.get("userId").toString(),
+                m -> m.get("userName").toString(),
+                (oldValue, newValue) -> oldValue));
+
+        data.put("userNameMap", userNameMap);
+        // 2. 复现 task_analysis_5_1(按任务类型的统计分析)
+        Map<String, List<List<Object>>> taskAnalysis51 = new HashMap<>();
+        Map<String, Integer> mpParentTypeCount = new HashMap<>();
+        Map<String, Long> mpParentTypeScore = new HashMap<>();
+
+        // 解析taskStatsByType(protobuf中是列表,实际应为单个对象,取第一个)
+        if (!taskStatsByTypeList.isEmpty()) {
+            Map<String, Object> taskStatsByType = taskStatsByTypeList.get(0);
+            Map<String, Map<String, Object>> taskStatsMap =
+                (Map<String, Map<String, Object>>) taskStatsByType.get("taskStats");
+
+            for (Map.Entry<String, Map<String, Object>> entry : taskStatsMap.entrySet()) {
+                String taskTypeId = entry.getKey();
+                Map<String, Object> taskStat = entry.getValue();
+                int count = (Integer) taskStat.get("count");
+                Object o = taskStat.get("score");
+                Long score = Longs.tryParse(o.toString());
+
+                // 从taskTypeMetas中获取父/子分类名称(根据taskTypeId匹配)
+                String parentName = "";
+                String childName = "";
+                for (Map<String, Object> meta : taskTypeMetas) {
+                    if (meta.get("taskTypeId").equals(taskTypeId)) {
+                        parentName = (String) meta.get("parentName");
+                        childName = (String) meta.get("childName");
+                        break;
+                    }
+                }
+
+                // 构建行数据
+                List<Object> row = new ArrayList<>();
+                row.add(childName);
+                row.add(count);
+                row.add(count * 1.0 / approvedCnt); // 占比=当前类型数量/总通过数量
+                row.add(score);
+
+                row.add(score * 1d / scoreSum); // 积分占比
+
+                // 按父分类分组
+                List<List<Object>> rowList = taskAnalysis51.getOrDefault(parentName, new ArrayList<>());
+                rowList.add(row);
+                taskAnalysis51.put(parentName, rowList);
+
+                // 更新父分类的总数量和积分
+                mpParentTypeCount.put(parentName, mpParentTypeCount.getOrDefault(parentName, 0) + count);
+                mpParentTypeScore.put(parentName, mpParentTypeScore.getOrDefault(parentName, 0L) + score);
+            }
+        }
+        data.put("task_analysis_5_1", taskAnalysis51);
+
+        // 复现 task_analysis_5_1 的最大值(最多任务的父分类)
+        if (!mpParentTypeCount.isEmpty()) {
+
+            Map<String, Integer> starTaskMap = (Map<String, Integer>) data.get("starTaskMap");
+            Map.Entry<String, Integer> maxEntry = starTaskMap.entrySet().iterator().next();
+            data.put("task_analysis_5_1_maxName", maxEntry.getKey());
+            data.put("task_analysis_5_1_maxCount", maxEntry.getValue().toString());
+            // 计算最大父分类的积分占比
+            long maxParentScore = mpParentTypeScore.getOrDefault(maxEntry.getKey(), 0L);
+            data.put("task_analysis_5_1_maxPercent", String.format("%.2f", maxParentScore * 1.0 / scoreSum * 100));
+        }
+
+        // 3. 复现 task_promotion_count_5_1_1(按任务类型的推广人统计)
+        Map<String, List<List<Object>>> taskPromotionCount511 = new HashMap<>();
+        Set<String> allPromotioners = new HashSet<>(); // 所有推广人ID
+
+        if (!taskStatsByTypeList.isEmpty()) {
+            Map<String, Object> taskStatsByType = taskStatsByTypeList.get(0);
+            Map<String, Map<String, Object>> taskStatsMap =
+                (Map<String, Map<String, Object>>) taskStatsByType.get("taskStats");
+
+            for (Map.Entry<String, Map<String, Object>> entry : taskStatsMap.entrySet()) {
+                String taskTypeId = entry.getKey();
+                Map<String, Object> taskStat = entry.getValue();
+                int count = (Integer) taskStat.get("count");
+                List<String> promotioners = (List<String>) taskStat.get("promotioners"); // 该任务类型的推广人列表
+                if (promotioners != null) {
+                    allPromotioners.addAll(promotioners);
+                }
+
+                // 从taskTypeMetas中获取父/子分类名称
+                String parentName = "";
+                String childName = "";
+                for (Map<String, Object> meta : taskTypeMetas) {
+                    if (meta.get("taskTypeId").equals(taskTypeId)) {
+                        parentName = (String) meta.get("parentName");
+                        childName = (String) meta.get("childName");
+                        break;
+                    }
+                }
+
+                // 构建行数据
+                List<Object> row = new ArrayList<>();
+                row.add(childName);
+                row.add(promotioners != null ? promotioners.size() : 0); // 推广人数
+                row.add(count); // 任务数量
+                row.add(promotioners != null && !promotioners.isEmpty() ? count * 1.0 / promotioners.size() : 0); // 人均任务数
+
+                // 按父分类分组
+                List<List<Object>> rowList = taskPromotionCount511.getOrDefault(parentName, new ArrayList<>());
+                rowList.add(row);
+                taskPromotionCount511.put(parentName, rowList);
+            }
+        }
+        data.put("task_promotion_count_5_1_1", taskPromotionCount511);
+
+        // 复现推广人平均任务数
+        int promotionerCount = allPromotioners.size();
+        String avgByPerson =
+            promotionerCount > 0 ? String.format("%.2f", approvedCnt * 1.0 / promotionerCount) : "0.00";
+        data.put("task_promotion_count_5_1_1_avg", avgByPerson);
+
+        // 复现task_promotion_count_5_1_1_last(合计行)
+        List<Object> lastRow511 = new ArrayList<>();
+        lastRow511.add(null); // label
+        lastRow511.add(promotionerCount); // 总推广人数
+        lastRow511.add(approvedCnt); // 总任务数
+        lastRow511.add(avgByPerson); // 平均任务数
+        data.put("task_promotion_count_5_1_1_last", lastRow511);
+
+        // 4. 复现 task_promotioner_5_4(按推广人的任务类型统计)
+        Map<String, List<List<Object>>> taskPromotioner54 = new HashMap<>();
+
+        if (!taskStatsByPersonList.isEmpty()) {
+            Map<String, Object> taskStatsByPerson = taskStatsByPersonList.get(0);
+            Map<String, Map<String, Object>> userStatsMap =
+                (Map<String, Map<String, Object>>) taskStatsByPerson.get("userStats");
+
+            for (Map.Entry<String, Map<String, Object>> userEntry : userStatsMap.entrySet()) {
+                String userId = userEntry.getKey();
+                Map<String, Object> userTaskStats = userEntry.getValue();
+                Map<String, Map<String, Object>> taskStatsByUser =
+                    (Map<String, Map<String, Object>>) userTaskStats.get("taskStats");
+
+                // 获取用户名(从taskTypeMetas中匹配)
+                // String userName = "无姓名";
+                // for (Map<String, Object> meta : taskTypeMetas) {
+                // if (meta.get("userId").equals(userId)) {
+                // userName = (String) meta.get("userName");
+                // break;
+                // }
+                // }
+
+                // 构建该用户的任务统计行
+                List<List<Object>> rowList = new ArrayList<>();
+                for (Map.Entry<String, Map<String, Object>> taskEntry : taskStatsByUser.entrySet()) {
+                    String taskTypeId = taskEntry.getKey();
+                    Map<String, Object> taskStat = taskEntry.getValue();
+                    int count = (Integer) taskStat.get("count");
+                    Object o = taskStat.get("score");
+                    Long score = Longs.tryParse(o.toString());
+                    // 获取任务类型名称
+                    String taskTypeName = "";
+                    for (Map<String, Object> meta : taskTypeMetas) {
+                        if (meta.get("taskTypeId").equals(taskTypeId)) {
+                            taskTypeName = (String) meta.get("childName");
+                            break;
+                        }
+                    }
+
+                    List<Object> row = new ArrayList<>();
+                    row.add(taskTypeName);
+                    row.add(count);
+                    row.add(count * 1.0 / approvedCnt); // 任务占比
+                    row.add(score);
+                    row.add(score * 1d / scoreSum);
+                    rowList.add(row);
+                }
+                taskPromotioner54.put(userId, rowList);
+            }
+        }
+        data.put("task_promotioner_5_4", taskPromotioner54);
+
+        // 5. 复现 task_promotioner_quality_5_4_1(推广人质量统计)
+        List<Map<String, Object>> taskPromotionerQuality541 = new ArrayList<>();
+        int idx = 1;
+
+        if (!taskStatsByPersonList.isEmpty()) {
+            Map<String, Object> taskStatsByPerson = taskStatsByPersonList.get(0);
+            Map<String, Map<String, Object>> userStatsMap =
+                (Map<String, Map<String, Object>>) taskStatsByPerson.get("userStats");
+
+            for (Map.Entry<String, Map<String, Object>> userEntry : userStatsMap.entrySet()) {
+                String userId = userEntry.getKey();
+                Map<String, Object> userTaskStats = userEntry.getValue();
+                Map<String, Map<String, Object>> taskStatsByUser =
+                    (Map<String, Map<String, Object>>) userTaskStats.get("taskStats");
+
+                // 计算该用户的总通过任务数和积分
+                int userApprovedCnt = 0;
+                long userApprovedScore = 0L;
+                for (Map<String, Object> taskStat : taskStatsByUser.values()) {
+                    userApprovedCnt += (Integer) taskStat.get("count");
+                    userApprovedScore += Longs.tryParse(taskStat.get("score").toString());
+
+                }
+
+                int userSubmittedCnt = 0;
+                int userSubmittedScore = 0;
+                // 获取用户名
+                String userName = "无姓名";
+                for (Map<String, Object> meta : taskTypeMetas) {
+                    if (meta.get("userId").equals(userId)) {
+                        userSubmittedCnt = userScoreMap.get(meta.get("userId").toString()).size();
+                        userSubmittedScore =
+                            userScoreMap.get(meta.get("userId")).stream().mapToInt(Integer::intValue).sum();
+                        userName = (String) meta.get("userName");
+                        break;
+                    }
+                }
+
+                // 构建行数据
+                Map<String, Object> row = new HashMap<>();
+                row.put("seq", idx++);
+                row.put("promotioner", userName);
+                row.put("submittedCnt", userSubmittedCnt);
+                row.put("approvedCnt", userApprovedCnt);
+                row.put("taskRate", userSubmittedCnt > 0
+                    ? String.format("%.2f%%", userApprovedCnt * 1.0 / userSubmittedCnt * 100) : "0.00%");
+                row.put("submittedScore", userSubmittedScore);
+                row.put("approvedScore", userApprovedScore);
+                row.put("scoreRate", userSubmittedScore > 0
+                    ? String.format("%.2f%%", userApprovedScore * 1.0 / userSubmittedScore * 100) : "0.00%");
+                taskPromotionerQuality541.add(row);
+            }
+        }
+
+        data.put("task_promotioner_quality_5_4_1", taskPromotionerQuality541);
+
+        // 6. 复现 task_promotioner_quality_5_4_1_last(合计行)
+        List<Object> lastRow541 = new ArrayList<>();
+        lastRow541.add(null); // label
+        lastRow541.add(submittedCnt); // 总提交数
+        lastRow541.add(approvedCnt); // 总通过数
+        String taskRate = submittedCnt > 0 ? String.format("%.2f%%", approvedCnt * 1.0 / submittedCnt * 100) : "0.00%";
+        lastRow541.add(taskRate);
+        Long totalSubmittedScore = Longs.tryParse(scoreSum.toString());
+        lastRow541.add(totalSubmittedScore);
+        lastRow541.add(scoreSum); // 总通过积分
+        String scoreRate =
+            totalSubmittedScore > 0 ? String.format("%.2f%%", scoreSum * 1.0 / totalSubmittedScore * 100) : "0.00%";
+        lastRow541.add(scoreRate);
+        data.put("task_promotioner_quality_5_4_1_last", lastRow541);
+
+        // 7. 填充其他字段
+        data.put("task_promotioner_quality_5_4_1_taskRate", taskRate.replace("%", ""));
+        data.put("task_promotioner_quality_5_4_1_scoreRate", scoreRate.replace("%", ""));
+        data.put("task_promotioner_quality_5_4_1_approvedCnt", approvedCnt);
+        data.put("task_promotioner_quality_5_4_1_submittedCnt", submittedCnt);
+        data.put("task_promotioner_quality_5_4_1_rejectedCnt", rejectedCnt);
+    }
+
+}

+ 8 - 6
easier-report-biz/src/main/java/com/yaoyicloud/render/foundation/FoundationBasicInfoRender.java

@@ -28,8 +28,10 @@ import org.apache.commons.collections4.MapUtils;
 @Slf4j
 public final class FoundationBasicInfoRender extends AbstractRender {
     private final FilerepoProperties filerepoProperties;
-private final CommonDataCache commonDataCache;
-    public FoundationBasicInfoRender(String cwd, FilerepoProperties filerepoProperties, CommonDataCache commonDataCache) {
+    private final CommonDataCache commonDataCache;
+
+    public FoundationBasicInfoRender(String cwd, FilerepoProperties filerepoProperties,
+        CommonDataCache commonDataCache) {
         super(cwd);
         this.filerepoProperties = filerepoProperties;
         this.commonDataCache = commonDataCache;
@@ -57,11 +59,11 @@ private final CommonDataCache commonDataCache;
         String relationId) throws IOException {
         log.info("开始渲染基金会基础信息报告模块,relationId: {}", relationId);
 
-
         // 配置POI-TL渲染器
         ConfigureBuilder builder = Configure.builder();
         RenderPolicy indicatorsRenderPolicyToProtobuf = this.indicatorsRenderPolicyToProtobuf();
-        builder.bind("basicInfoChecks", indicatorsRenderPolicyToProtobuf).bind("superiorAuthority", indicatorsRenderPolicyToProtobuf);
+        builder.bind("basicInfoChecks", indicatorsRenderPolicyToProtobuf).bind("superiorAuthority",
+            indicatorsRenderPolicyToProtobuf);
         builder.addPlugin('^', this.pictureRenderPolicy());
         builder.useSpringEL();
         // 通过默认protobuf实例来填充不存在的key
@@ -73,8 +75,8 @@ private final CommonDataCache commonDataCache;
             .build();
 
         String completeJson = JsonFormat.printer()
-                .includingDefaultValueFields()
-                .print(mergedProto);
+            .includingDefaultValueFields()
+            .print(mergedProto);
 
         ObjectMapper objectMapper = new ObjectMapper();
         Map<String, Object> data = objectMapper.readValue(completeJson, new TypeReference<Map<String, Object>>() {});

+ 16 - 0
easier-report-biz/src/main/java/com/yaoyicloud/service/CsoReportService.java

@@ -0,0 +1,16 @@
+package com.yaoyicloud.service;
+
+import com.yaoyicloud.constant.enums.ModuleType;
+import com.yaoyicloud.entity.ReportGenerationResult;
+
+
+public interface CsoReportService {
+
+    ReportGenerationResult createCsoCheckReport(
+            String data,
+            byte[] templateBytes,
+            Long relationId,
+            ModuleType moduleType
+    ) throws Exception;
+
+}

+ 1 - 3
easier-report-biz/src/main/java/com/yaoyicloud/service/ReportUpdateService.java

@@ -1,10 +1,8 @@
 package com.yaoyicloud.service;
 
 import com.yaoyicloud.constant.enums.ModuleType;
-import com.yaoyicloud.constant.enums.ReportType;
-import com.yaoyicloud.entity.ReportGenerationResult;
 
-import javax.servlet.http.HttpServletRequest;
+import com.yaoyicloud.entity.ReportGenerationResult;
 
 /**
  * 报告服务

+ 111 - 0
easier-report-biz/src/main/java/com/yaoyicloud/service/impl/CsoReportServiceImpl.java

@@ -0,0 +1,111 @@
+package com.yaoyicloud.service.impl;
+
+import static com.yaoyicloud.config.SessionInterceptor.SESSION_MAP;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.yaoyicloud.render.cso.EntHeaderSectionRender;
+import com.yaoyicloud.render.cso.EntPromotionSectionRender;
+import com.yaoyicloud.service.CsoReportService;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.springframework.stereotype.Service;
+
+import com.yaoyicloud.config.CommonDataCache;
+import com.yaoyicloud.config.FilerepoProperties;
+import com.yaoyicloud.config.ReportPathManager;
+import com.yaoyicloud.constant.enums.ModuleType;
+import com.yaoyicloud.entity.ReportGenerationResult;
+
+import com.yaoyicloud.tools.DocxUtil;
+
+import cn.hutool.core.util.IdUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CsoReportServiceImpl implements CsoReportService {
+    // 使用ConcurrentHashMap存储relationId与计数器的映射
+    private final ConcurrentMap<Long, AtomicInteger> moduleCounters = new ConcurrentHashMap<>();
+
+    private final FilerepoProperties filerepoProperties;
+    private final ReportPathManager reportPathManager;
+    private final CommonDataCache commonDataCache;
+
+    /**
+     * 处理单个模块的报告生成
+     */
+    @SuppressWarnings("checkstyle:ParameterNumber")
+    private String processModule(ModuleType moduleType, String data,
+        byte[] templateBytes, String sessionId, Long relationId, AtomicInteger counter) throws Exception {
+        String reportPath;
+
+        switch (moduleType) {
+            case CSO_HEADER:
+                reportPath = new EntHeaderSectionRender(sessionId, filerepoProperties, commonDataCache)
+                    .renderDocx(data, templateBytes, String.valueOf(relationId));
+                break;
+            case CSO_PROMOTION_SUMMARY:
+                reportPath = new EntPromotionSectionRender(sessionId, filerepoProperties, commonDataCache)
+                        .renderDocx(data, templateBytes, String.valueOf(relationId));
+                break;
+
+            default:
+                throw new UnsupportedOperationException("Unsupported module type: " + moduleType);
+        }
+        return reportPath;
+    }
+
+
+    @Override
+    public ReportGenerationResult createCsoCheckReport(String data, byte[] templateBytes, Long relationId, ModuleType moduleType) throws Exception {
+        // 1. 将字节流模版写入临时文件
+        File tempTemplateFile = File.createTempFile("template_", ".docx");
+        try (FileOutputStream fos = new FileOutputStream(tempTemplateFile)) {
+            fos.write(templateBytes);
+        }
+
+        // 获取或初始化计数器
+        AtomicInteger counter = moduleCounters.computeIfAbsent(
+                relationId,
+                k -> new AtomicInteger(1));
+        String sessionId = SESSION_MAP.get(relationId.toString());
+        String reportPath = null;
+
+        reportPath = processModule(moduleType, data, templateBytes, sessionId, relationId, counter);
+        // 处理指定模块
+        reportPathManager.addReportPath(sessionId, reportPath);
+        counter.incrementAndGet();
+
+        String mergedReportPath = null;
+        boolean isLastModule = reportPath.contains("attachmentSection");
+
+        if (isLastModule) {
+            List<String> reportPaths = reportPathManager.getReportPaths(sessionId);
+            XWPFDocument targetDoc;
+            try (FileInputStream fis = new FileInputStream(reportPaths.get(0))) {
+                targetDoc = new XWPFDocument(fis);
+            }
+            List<XWPFDocument> sourceDocs = new ArrayList<>();
+            for (int i = 1; i < reportPaths.size(); i++) {
+                try (FileInputStream fis = new FileInputStream(reportPaths.get(i))) {
+                    sourceDocs.add(new XWPFDocument(fis));
+                }
+            }
+            String label = relationId + "_" + IdUtil.fastSimpleUUID();
+            mergedReportPath = filerepoProperties.getBasePath() + "/" + label + ".docx";
+            // 调用合并方法
+            DocxUtil.mergeDocx(targetDoc, sourceDocs, mergedReportPath, relationId);
+        }
+
+        return new ReportGenerationResult(reportPath, mergedReportPath, isLastModule);
+    }
+}

+ 1 - 4
easier-report-biz/src/main/java/com/yaoyicloud/service/impl/ReportUpdateServiceImpl.java

@@ -64,7 +64,6 @@ public class ReportUpdateServiceImpl implements ReportUpdateService {
         String data, byte[] templateBytes, Long relationId,
         ModuleType moduleType)
         throws Exception {
-
         // 1. 将字节流模版写入临时文件
         File tempTemplateFile = File.createTempFile("template_", ".docx");
         try (FileOutputStream fos = new FileOutputStream(tempTemplateFile)) {
@@ -76,9 +75,7 @@ public class ReportUpdateServiceImpl implements ReportUpdateService {
             relationId,
             k -> new AtomicInteger(1));
         String sessionId = SESSION_MAP.get(relationId.toString());
-        String reportPath = null;
-
-        reportPath = processModule(moduleType, data, templateBytes, sessionId, relationId, counter);
+        String reportPath = processModule(moduleType, data, templateBytes, sessionId, relationId, counter);
         // 处理指定模块
         reportPathManager.addReportPath(sessionId, reportPath);
         counter.incrementAndGet();

+ 84 - 0
easier-report-biz/src/main/proto/CsoEntReport.proto

@@ -0,0 +1,84 @@
+syntax = "proto3";
+
+package report;
+
+option java_package = "com.yaoyicloud.message";
+option java_outer_classname = "CSOProtos";
+
+// 报告基础信息(封面数据)
+message ReportHeader {
+  optional string report_id = 1; // 报告ID
+  optional string report_create_time = 2; // 生成时间
+  optional string entrusting_project_time = 3; // 委托项目时间
+  optional string entrusting_company = 4; // 委托公司
+  optional string service_company = 5; // 服务公司
+  optional string drugs = 6; // 服务品种
+  optional string task_finish_time = 7; // 任务完成时间
+  optional int32 total_package_score = 8; // 积分包值
+  optional int32 total_approved_score = 11; // 审核通过总值
+  repeated DrugDetail drug_details = 12; // 产品列表(表格数据)
+  optional  string score_package_name = 13; // 积分包名称
+  optional  int32 task_user_names = 14; // 积分包推广人数
+  optional  int32 submited_task_score = 15; // 总提交积分
+  optional  string create_time = 16;         //创建时间
+  optional  string service_end_date = 17;         //完成时间
+  optional bool sensitiveFlag = 18; // 是否脱敏
+
+}
+
+// 产品明细
+message DrugDetail {
+  optional int32 index = 1; // 序号
+  optional string drug_name = 2; // 商品名称
+  optional string generic_name = 3; // 通用名
+  optional string drug_dose = 4; // 剂型
+  optional string drug_guige = 5; // 规格
+  optional string drug_ent_name = 6; // 生产企业
+}
+
+// 3.推广总结
+message PromotionSummary {
+  optional  string create_time = 2; // 发包时间
+  optional  string service_end_date = 3; // 完成时间
+  optional  string task_user_names = 4; // 推广人员列表
+  optional  int32 task_promotioner_quality_5_4_1_submittedCnt = 5; // 总提交任务数
+  optional  int32 task_promotioner_quality_5_4_1_approvedCnt = 7; // 审核通过任务数
+  optional  int32 submited_task_score = 9; // 总提交积分
+  optional  int32 total_approved_score = 11; // 审核通过积分
+  repeated TaskStatisticsByType task_stats_by_type = 22;
+  repeated TaskStatisticsByPerson task_stats_by_person = 23;
+  repeated TaskTypeMeta task_type_metas = 24;
+  map<string, int32> starTaskMap = 25; //本次推广服务项目中完成最多的任务 和任务次数
+  //optional int64 scoreSum =26; //所有任务的总积分之和
+  map<string, bytes> userSubmittedScoreMap = 27;  // 用户id 和 积分列表
+}
+
+// 任务统计信息
+message TaskStatistics {
+  int32 count = 1;                  // 推广次数
+  int64 score = 2;                  // 推广积分值
+  repeated string promotioners = 3; // 推广人员ID集合
+  map<string, int32> mpProductCount = 4; // 产品ID到数量的映射
+}
+
+// 按任务类型的统计信息 (对应 mpTaskStat)
+message TaskStatisticsByType {
+  map<string, TaskStatistics> task_stats = 1; // 任务类型ID -> 统计信息
+  // repeated TaskTypeMeta task_type_metas = 2;
+}
+
+// 按用户和任务类型的统计信息 (对应 mpTaskStatbyPerson)
+message TaskStatisticsByPerson {
+  map<string, TaskStatisticsByType> user_stats = 1; // 用户ID -> 用户任务统计
+  // repeated TaskTypeMeta task_type_metas = 2;
+
+}
+//任务类型的元数据(关联ID与名称)
+message TaskTypeMeta {
+  string task_type_id = 1;        // 任务类型ID(主键)
+  string parent_name = 2;         // 父分类名(如“医疗终端客户拜访”)
+  string child_name = 3;          // 子任务类型名(如“医疗终端客户拜访”)
+  string user_id = 4;             //推广人id
+  string user_name = 5;          //推广人
+
+}

+ 7 - 0
pom.xml

@@ -76,6 +76,13 @@
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>29.0-jre</version>
+        </dependency>
     </dependencies>
     <dependencyManagement>
         <dependencies>