Procházet zdrojové kódy

知识库,积分导出功能版本调整

徐滕 před 2 týdny
rodič
revize
9a1b990597

+ 261 - 17
src/main/java/com/jeeplus/common/utils/excel/ExportKnowledgeBaseExcel.java

@@ -27,7 +27,7 @@ import com.jeeplus.common.utils.excel.annotation.ExcelField;
 public class ExportKnowledgeBaseExcel {
 	private static final Logger log = LoggerFactory.getLogger(ExportKnowledgeBaseExcel.class);
 	private Workbook workbook;
-	private List<MapSheetData> mapSheetDataList = new ArrayList<>();
+	private List<Object> mapSheetDataList = new ArrayList<>();
 
 	/**
 	 * 内部类:存储单个Sheet的数据(Map模式)
@@ -47,6 +47,34 @@ public class ExportKnowledgeBaseExcel {
 	}
 
 	/**
+	 * 内部类:存储双表头Sheet的数据
+	 */
+	private static class DoubleHeaderSheetData {
+		String sheetName;
+		String title;
+		List<String> prefixHeaders;      // 前缀列的表头名和mapKey(格式:"中文名|mapKey")
+		List<String> firstRowHeaders;      // 第一行表头(如:上传文档数量、贡献积分额度等)
+		List<List<String>> secondRowHeaders; // 第二行表头列表,每个元素对应一个第一行表头的子列(如:总量+各分类节点)
+		List<String> columnKeys;            // 每个第一行表头对应的数据字段key
+		List<Map<String, Object>> dataList;
+
+		public DoubleHeaderSheetData(String sheetName, String title,
+									 List<String> prefixHeaders,
+									 List<String> firstRowHeaders,
+									 List<List<String>> secondRowHeaders,
+									 List<String> columnKeys,
+									 List<Map<String, Object>> dataList) {
+			this.sheetName = sheetName;
+			this.title = title;
+			this.prefixHeaders = prefixHeaders;
+			this.firstRowHeaders = firstRowHeaders;
+			this.secondRowHeaders = secondRowHeaders;
+			this.columnKeys = columnKeys;
+			this.dataList = dataList;
+		}
+	}
+
+	/**
 	 * 构造函数:初始化工作簿
 	 */
 	public ExportKnowledgeBaseExcel() {
@@ -62,6 +90,23 @@ public class ExportKnowledgeBaseExcel {
 	}
 
 	/**
+	 * 添加双表头Sheet页
+	 * @param sheetName Sheet名称
+	 * @param title 标题
+	 * @param firstRowHeaders 第一行表头列表(如:上传文档数量、贡献积分额度等)
+	 * @param secondRowHeaders 第二行表头列表,每个元素对应一个第一行表头的子列列表(如:总量+各分类节点)
+	 * @param dataList 数据列表
+	 */
+	public void addDoubleHeaderSheet(String sheetName, String title,
+									 List<String> prefixHeaders,
+									 List<String> firstRowHeaders,
+									 List<List<String>> secondRowHeaders,
+									 List<String> columnKeys,
+									 List<Map<String, Object>> dataList) {
+		mapSheetDataList.add(new DoubleHeaderSheetData(sheetName, title, prefixHeaders, firstRowHeaders, secondRowHeaders, columnKeys, dataList));
+	}
+
+	/**
 	 * 创建单元格样式
 	 */
 	private Map<String, CellStyle> createStyles(Workbook wb) {
@@ -82,23 +127,83 @@ public class ExportKnowledgeBaseExcel {
 		style.setFont(titleFont);
 		styles.put("title", style);
 
-		// 表头样式(无边框 + 更深底色 + 白色字体)
+		// 前缀列表头样式(浅灰底色 + 细边框 + 深色字体)
 		style = wb.createCellStyle();
 		style.setAlignment(HorizontalAlignment.CENTER);
 		style.setVerticalAlignment(VerticalAlignment.CENTER);
-		style.setBorderBottom(BorderStyle.NONE);
-		style.setBorderLeft(BorderStyle.NONE);
-		style.setBorderRight(BorderStyle.NONE);
-		style.setBorderTop(BorderStyle.NONE);
-		style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
+		style.setBorderBottom(BorderStyle.THIN);
+		style.setBorderLeft(BorderStyle.THIN);
+		style.setBorderRight(BorderStyle.THIN);
+		style.setBorderTop(BorderStyle.THIN);
+		style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
 		style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
-		Font headerFont = wb.createFont();
-		headerFont.setFontName("宋体");
-		headerFont.setFontHeightInPoints((short) 11);
-		headerFont.setBold(true);
-		headerFont.setColor(IndexedColors.WHITE.getIndex());
-		style.setFont(headerFont);
-		styles.put("header", style);
+		Font prefixHeaderFont = wb.createFont();
+		prefixHeaderFont.setFontName("宋体");
+		prefixHeaderFont.setFontHeightInPoints((short) 11);
+		prefixHeaderFont.setBold(true);
+		style.setFont(prefixHeaderFont);
+		styles.put("prefix_header", style);
+
+		// 业务列分组表头颜色(仅两种颜色交替:浅绿/浅黄,支持无限扩展)
+		short[] headerColors = {
+			IndexedColors.LIGHT_GREEN.getIndex(),   // 偶数索引组:浅绿
+			IndexedColors.LIGHT_YELLOW.getIndex()   // 奇数索引组:浅黄
+		};
+		for (int c = 0; c < headerColors.length; c++) {
+			style = wb.createCellStyle();
+			style.setAlignment(HorizontalAlignment.CENTER);
+			style.setVerticalAlignment(VerticalAlignment.CENTER);
+			style.setBorderBottom(BorderStyle.MEDIUM);
+			style.setBorderLeft(BorderStyle.MEDIUM);
+			style.setBorderRight(BorderStyle.MEDIUM);
+			style.setBorderTop(BorderStyle.MEDIUM);
+			style.setFillForegroundColor(headerColors[c]);
+			style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+			Font hFont = wb.createFont();
+			hFont.setFontName("宋体");
+			hFont.setFontHeightInPoints((short) 11);
+			hFont.setBold(true);
+			style.setFont(hFont);
+			styles.put("header_group_" + c, style);
+		}
+
+		// 业务列数据区域交替底色(与表头颜色完全一致,仅两种颜色交替)
+		short[] dataColors = {
+			IndexedColors.LIGHT_GREEN.getIndex(),   // 偶数索引组:浅绿
+			IndexedColors.LIGHT_YELLOW.getIndex()   // 奇数索引组:浅黄
+		};
+		for (int c = 0; c < dataColors.length; c++) {
+			// 字符串居中 + 分组底色
+			CellStyle ds = wb.createCellStyle();
+			ds.setAlignment(HorizontalAlignment.CENTER);
+			ds.setVerticalAlignment(VerticalAlignment.CENTER);
+			ds.setBorderBottom(BorderStyle.THIN);
+			ds.setBorderLeft(BorderStyle.THIN);
+			ds.setBorderRight(BorderStyle.THIN);
+			ds.setBorderTop(BorderStyle.THIN);
+			ds.setFillForegroundColor(dataColors[c]);
+			ds.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+			Font df = wb.createFont();
+			df.setFontName("宋体");
+			df.setFontHeightInPoints((short) 11);
+			ds.setFont(df);
+			styles.put("data_string_group_" + c, ds);
+			// 数字靠右 + 分组底色
+			CellStyle dn = wb.createCellStyle();
+			dn.setAlignment(HorizontalAlignment.RIGHT);
+			dn.setVerticalAlignment(VerticalAlignment.CENTER);
+			dn.setBorderBottom(BorderStyle.THIN);
+			dn.setBorderLeft(BorderStyle.THIN);
+			dn.setBorderRight(BorderStyle.THIN);
+			dn.setBorderTop(BorderStyle.THIN);
+			dn.setFillForegroundColor(dataColors[c]);
+			dn.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+			Font nf = wb.createFont();
+			nf.setFontName("宋体");
+			nf.setFontHeightInPoints((short) 11);
+			dn.setFont(nf);
+			styles.put("data_number_group_" + c, dn);
+		}
 
 		// 字符串居中样式
 		CellStyle stringStyle = wb.createCellStyle();
@@ -135,8 +240,12 @@ public class ExportKnowledgeBaseExcel {
 	 * 生成Excel文件
 	 */
 	public void write(OutputStream os) throws IOException {
-		for (MapSheetData sheetData : mapSheetDataList) {
-			createMapSheetContent(sheetData);
+		for (Object sheetData : mapSheetDataList) {
+			if (sheetData instanceof DoubleHeaderSheetData) {
+				createDoubleHeaderSheetContent((DoubleHeaderSheetData) sheetData);
+			} else if (sheetData instanceof MapSheetData) {
+				createMapSheetContent((MapSheetData) sheetData);
+			}
 		}
 		workbook.write(os);
 	}
@@ -181,7 +290,7 @@ public class ExportKnowledgeBaseExcel {
 			sheet.setColumnWidth(i, width * 256 * 2);
 
 			Cell headerCell = headerRow.createCell(i);
-			headerCell.setCellStyle(styles.get("header"));
+			headerCell.setCellStyle(styles.get("prefix_header"));
 			headerCell.setCellValue(displayHeader);
 		}
 
@@ -228,6 +337,141 @@ public class ExportKnowledgeBaseExcel {
 	}
 
 	/**
+	 * 创建双表头Sheet的内容
+	 * 支持前缀列(如部门名称、用户姓名等)+ 双表头业务列
+	 */
+	private void createDoubleHeaderSheetContent(DoubleHeaderSheetData sheetData) {
+		Sheet sheet = workbook.createSheet(sheetData.sheetName);
+		Map<String, CellStyle> styles = createStyles(workbook);
+		List<String> prefixHeaders = sheetData.prefixHeaders != null ? sheetData.prefixHeaders : new ArrayList<>();
+		List<String> firstRowHeaders = sheetData.firstRowHeaders;
+		List<List<String>> secondRowHeaders = sheetData.secondRowHeaders;
+		int prefixCount = prefixHeaders.size();
+		int rownum = 0;
+
+		// 计算总列数
+		int totalColumns = prefixCount;
+		for (List<String> subHeaders : secondRowHeaders) {
+			totalColumns += subHeaders.size();
+		}
+
+		// 创建标题行
+		if (StringUtils.isNotBlank(sheetData.title)) {
+			Row titleRow = sheet.createRow(rownum++);
+			titleRow.setHeightInPoints(30);
+			Cell titleCell = titleRow.createCell(0);
+			titleCell.setCellStyle(styles.get("title"));
+			titleCell.setCellValue(sheetData.title);
+			sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, totalColumns - 1));
+		}
+
+		// 创建第一行表头(前缀列 + 合并的业务表头)
+		Row firstHeaderRow = sheet.createRow(rownum++);
+		firstHeaderRow.setHeightInPoints(18);
+		int currentCol = 0;
+
+		// 前缀列在第一行表头留空(合并两行)
+		for (int p = 0; p < prefixCount; p++) {
+			sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum, currentCol, currentCol));
+			Cell headerCell = firstHeaderRow.createCell(currentCol);
+			headerCell.setCellStyle(styles.get("prefix_header"));
+			String displayHeader = prefixHeaders.get(p).contains("|") ? prefixHeaders.get(p).split("\\|")[0] : prefixHeaders.get(p);
+			headerCell.setCellValue(displayHeader);
+			int width = Math.max(displayHeader.length() * 2 + 2, 8);
+			sheet.setColumnWidth(currentCol, width * 256);
+			currentCol++;
+		}
+
+		// 业务列的第一行表头(合并单元格)
+		for (int i = 0; i < firstRowHeaders.size(); i++) {
+			String header = firstRowHeaders.get(i);
+			List<String> subHeaders = secondRowHeaders.get(i);
+			int subColumnCount = subHeaders.size();
+
+			int maxWidth = 0;
+			for (String subHeader : subHeaders) {
+				int width = 0;
+				for (char c : subHeader.toCharArray()) {
+					width += (c > 127) ? 2 : 1;
+				}
+				maxWidth = Math.max(maxWidth, width);
+			}
+			maxWidth = Math.max(maxWidth, 10);
+			maxWidth = Math.min(maxWidth, 50);
+
+			if (subColumnCount > 1) {
+				sheet.addMergedRegion(new CellRangeAddress(rownum - 1, rownum - 1, currentCol, currentCol + subColumnCount - 1));
+			}
+
+			Cell headerCell = firstHeaderRow.createCell(currentCol);
+			headerCell.setCellStyle(styles.get("header_group_" + (i % 2)));
+			headerCell.setCellValue(header);
+
+			int columnWidth = maxWidth * 256 * 2 / subColumnCount;
+			for (int j = 0; j < subColumnCount; j++) {
+				sheet.setColumnWidth(currentCol + j, columnWidth);
+			}
+			currentCol += subColumnCount;
+		}
+
+		// 创建第二行表头(前缀列留空 + 业务子列头)
+		Row secondHeaderRow = sheet.createRow(rownum++);
+		secondHeaderRow.setHeightInPoints(18);
+		currentCol = 0;
+		// 前缀列在第二行已被合并,无需重复创建
+		currentCol = prefixCount;
+		for (int gi = 0; gi < secondRowHeaders.size(); gi++) {
+			List<String> subHeaders = secondRowHeaders.get(gi);
+			CellStyle subHeaderStyle = styles.get("header_group_" + (gi % 2));
+			for (String subHeader : subHeaders) {
+				Cell headerCell = secondHeaderRow.createCell(currentCol++);
+				headerCell.setCellStyle(subHeaderStyle);
+				headerCell.setCellValue(subHeader);
+			}
+		}
+
+		// 填充数据行
+		if (sheetData.dataList != null && !sheetData.dataList.isEmpty()) {
+			for (Map<String, Object> dataMap : sheetData.dataList) {
+				Row dataRow = sheet.createRow(rownum++);
+				dataRow.setHeightInPoints(16);
+				currentCol = 0;
+
+				// 前缀列数据
+				for (String prefixHeader : prefixHeaders) {
+					String mapKey = prefixHeader.contains("|") ? prefixHeader.split("\\|")[1] : prefixHeader;
+					Object value = dataMap.get(mapKey);
+					Cell cell = dataRow.createCell(currentCol++);
+					if (value instanceof Number) {
+						cell.setCellStyle(styles.get("data_number"));
+					} else {
+						cell.setCellStyle(styles.get("data_string"));
+					}
+					setMapCellValue(cell, value);
+				}
+
+				// 业务列数据(按分组使用交替底色:浅绿/浅黄)
+				for (int i = 0; i < sheetData.secondRowHeaders.size(); i++) {
+					List<String> subHeaders = sheetData.secondRowHeaders.get(i);
+					String columnKey = (sheetData.columnKeys != null && i < sheetData.columnKeys.size()) ? sheetData.columnKeys.get(i) : "";
+					int groupIdx = i % 2;
+					for (String subHeader : subHeaders) {
+						String dataKey = columnKey.isEmpty() ? subHeader : columnKey + "_" + subHeader;
+						Object value = dataMap.get(dataKey);
+						Cell cell = dataRow.createCell(currentCol++);
+						if (value instanceof Number) {
+							cell.setCellStyle(styles.get("data_number_group_" + groupIdx));
+						} else {
+							cell.setCellStyle(styles.get("data_string_group_" + groupIdx));
+						}
+						setMapCellValue(cell, value);
+					}
+				}
+			}
+		}
+	}
+
+	/**
 	 * 输出到客户端下载
 	 */
 	public void write(HttpServletResponse response, String fileName) throws IOException {

+ 77 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBasePointDetailDao.java

@@ -94,4 +94,81 @@ public interface WorkKnowledgeBasePointDetailDao extends CrudDao<WorkKnowledgeBa
      * @return 员工积分统计列表,按部门、姓名排序
      */
     List<WorkKnowledgeBasePoint> findAllEmployeePointStats(WorkKnowledgeBasePoint entity);
+
+    /**
+     * 根据知识库树节点名称列表和用户ID列表,统计各类别的积分总和(用于双表头导出)
+     * @param treeNames 树节点名称列表
+     * @param userIds 用户ID列表
+     * @param beginDate 开始日期(可选)
+     * @param endDate 结束日期(可选)
+     * @return Map列表,每个Map包含userId、treeName、totalPoint
+     */
+    List<java.util.Map<String, Object>> sumPointsByTreeNodeIds(
+        @Param("treeNames") List<String> treeNames,
+        @Param("userIds") List<String> userIds,
+        @Param("beginDate") String beginDate,
+        @Param("endDate") String endDate
+    );
+
+    /**
+     * 根据知识库树节点名称列表和用户ID列表,按change_type统计各类别的积分总和
+     * @param treeNames 树节点名称列表
+     * @param userIds 用户ID列表
+     * @param changeType 变动类型(1-创建,2-审核,3-阅读,4-点赞)
+     * @param beginDate 开始日期(可选)
+     * @param endDate 结束日期(可选)
+     * @return Map列表,每个Map包含userId、treeName、totalPoint
+     */
+    List<java.util.Map<String, Object>> sumPointsByTreeNodeIdsAndChangeType(
+        @Param("treeNames") List<String> treeNames,
+        @Param("userIds") List<String> userIds,
+        @Param("changeType") Integer changeType,
+        @Param("beginDate") String beginDate,
+        @Param("endDate") String endDate
+    );
+
+    /**
+     * 根据知识库树节点名称列表和用户ID列表,统计各类别的兑换积分总和(point<0)
+     * @param treeNames 树节点名称列表
+     * @param userIds 用户ID列表
+     * @param beginDate 开始日期(可选)
+     * @param endDate 结束日期(可选)
+     * @return Map列表,每个Map包含userId、treeName、totalPoint
+     */
+    List<java.util.Map<String, Object>> sumExchangePointsByTreeNodeIds(
+        @Param("treeNames") List<String> treeNames,
+        @Param("userIds") List<String> userIds,
+        @Param("beginDate") String beginDate,
+        @Param("endDate") String endDate
+    );
+
+    /**
+     * 根据知识库树节点名称列表和用户ID列表,统计各类别的上传文档数量
+     * @param treeNames 树节点名称列表
+     * @param userIds 用户ID列表
+     * @param beginDate 开始日期(可选)
+     * @param endDate 结束日期(可选)
+     * @return Map列表,每个Map包含userId、treeName、docCount
+     */
+    List<java.util.Map<String, Object>> countUploadDocsByTreeNodeIds(
+        @Param("treeNames") List<String> treeNames,
+        @Param("userIds") List<String> userIds,
+        @Param("beginDate") String beginDate,
+        @Param("endDate") String endDate
+    );
+
+    /**
+     * 根据知识库树节点名称列表和用户ID列表,统计各类别的审核文档数量
+     * @param treeNames 树节点名称列表
+     * @param userIds 用户ID列表
+     * @param beginDate 开始日期(可选)
+     * @param endDate 结束日期(可选)
+     * @return Map列表,每个Map包含userId、treeName、docCount
+     */
+    List<java.util.Map<String, Object>> countAuditDocsByTreeNodeIds(
+        @Param("treeNames") List<String> treeNames,
+        @Param("userIds") List<String> userIds,
+        @Param("beginDate") String beginDate,
+        @Param("endDate") String endDate
+    );
 }

+ 6 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseTreeInfoDao.java

@@ -54,4 +54,10 @@ public interface WorkKnowledgeBaseTreeInfoDao extends CrudDao<WorkKnowledgeBaseT
      * 根据根节点ID查询所有子孙节点(不含根节点本身)
      */
     List<WorkKnowledgeBaseTreeInfo> findAllDescendantsByRootId(@Param("rootId") String rootId);
+
+    /**
+     * 查询所有一级分类(parent_id='0')及其直接子节点名称列表
+     * @return Map结构:key为一级分类tree_name,value为该分类下所有子节点的tree_name列表
+     */
+    List<java.util.Map<String, Object>> findFirstLevelCategoriesWithChildren();
 }

+ 56 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBasePointDetailService.java

@@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -120,4 +121,59 @@ public class WorkKnowledgeBasePointDetailService extends CrudService<WorkKnowled
     public List<WorkKnowledgeBasePoint> findAllEmployeePointStats(WorkKnowledgeBasePoint entity) {
         return pointDetailDao.findAllEmployeePointStats(entity);
     }
+
+    /**
+     * 根据知识库树节点名称列表和用户ID列表,统计各类别的积分总和(用于双表头导出)
+     * @param treeNames 树节点名称列表
+     * @param userIds 用户ID列表
+     * @param beginDate 开始日期(可选)
+     * @param endDate 结束日期(可选)
+     * @return Map列表,每个Map包含userId、treeName、totalPoint
+     */
+    public List<java.util.Map<String, Object>> sumPointsByTreeNodeIds(List<String> treeNames, List<String> userIds, String beginDate, String endDate) {
+        if (treeNames == null || treeNames.isEmpty() || userIds == null || userIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return pointDetailDao.sumPointsByTreeNodeIds(treeNames, userIds, beginDate, endDate);
+    }
+
+    /**
+     * 按change_type统计各类别的积分总和
+     */
+    public List<java.util.Map<String, Object>> sumPointsByTreeNodeIdsAndChangeType(List<String> treeNames, List<String> userIds, Integer changeType, String beginDate, String endDate) {
+        if (treeNames == null || treeNames.isEmpty() || userIds == null || userIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return pointDetailDao.sumPointsByTreeNodeIdsAndChangeType(treeNames, userIds, changeType, beginDate, endDate);
+    }
+
+    /**
+     * 统计各类别的兑换积分总和(point<0)
+     */
+    public List<java.util.Map<String, Object>> sumExchangePointsByTreeNodeIds(List<String> treeNames, List<String> userIds, String beginDate, String endDate) {
+        if (treeNames == null || treeNames.isEmpty() || userIds == null || userIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return pointDetailDao.sumExchangePointsByTreeNodeIds(treeNames, userIds, beginDate, endDate);
+    }
+
+    /**
+     * 统计各类别的上传文档数量
+     */
+    public List<java.util.Map<String, Object>> countUploadDocsByTreeNodeIds(List<String> treeNames, List<String> userIds, String beginDate, String endDate) {
+        if (treeNames == null || treeNames.isEmpty() || userIds == null || userIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return pointDetailDao.countUploadDocsByTreeNodeIds(treeNames, userIds, beginDate, endDate);
+    }
+
+    /**
+     * 统计各类别的审核文档数量
+     */
+    public List<java.util.Map<String, Object>> countAuditDocsByTreeNodeIds(List<String> treeNames, List<String> userIds, String beginDate, String endDate) {
+        if (treeNames == null || treeNames.isEmpty() || userIds == null || userIds.isEmpty()) {
+            return new ArrayList<>();
+        }
+        return pointDetailDao.countAuditDocsByTreeNodeIds(treeNames, userIds, beginDate, endDate);
+    }
 }

+ 26 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseTreeService.java

@@ -198,4 +198,30 @@ public class WorkKnowledgeBaseTreeService extends CrudService<WorkKnowledgeBaseT
         workattachmentService.workAttachmentManage(list);
         return list;
     }
+
+    /**
+     * 获取所有一级分类及其子节点名称列表(用于导出双表头)
+     * @return Map结构:key为一级分类tree_name,value为该分类下所有子节点的tree_name列表
+     */
+    public java.util.Map<String, List<String>> getCategoryTreeForExport() {
+        // 查询返回扁平结构:每行包含 parentName 和 childName
+        List<java.util.Map<String, Object>> resultList = dao.findFirstLevelCategoriesWithChildren();
+        // 使用LinkedHashMap保持排序顺序
+        java.util.Map<String, List<String>> categoryMap = new java.util.LinkedHashMap<>();
+
+        for (java.util.Map<String, Object> item : resultList) {
+            String parentName = (String) item.get("parentName");
+            String childName = (String) item.get("childName");
+
+            // 保证每个一级分类至少有一个条目
+            categoryMap.putIfAbsent(parentName, new ArrayList<>());
+
+            // LEFT JOIN 可能产生 childName=null 的行(一级分类无子节点时)
+            if (childName != null && !childName.isEmpty()) {
+                categoryMap.get(parentName).add(childName);
+            }
+        }
+
+        return categoryMap;
+    }
 }

+ 297 - 68
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBasePointController.java

@@ -15,6 +15,7 @@ import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointDetailService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointExchangeService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointService;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBaseTreeService;
 import com.jeeplus.modules.sys.entity.User;
 import com.jeeplus.modules.sys.service.OfficeService;
 import com.jeeplus.modules.sys.utils.UserUtils;
@@ -57,6 +58,9 @@ public class WorkKnowledgeBasePointController extends BaseController {
     @Autowired
     private WorkKnowledgeBasePointExchangeService pointExchangeService;
 
+    @Autowired
+    private WorkKnowledgeBaseTreeService treeService;
+
     @ModelAttribute
     public WorkKnowledgeBasePoint get(@RequestParam(required=false) String id) {
         WorkKnowledgeBasePoint entity = null;
@@ -308,93 +312,318 @@ public class WorkKnowledgeBasePointController extends BaseController {
             // 查询所有员工积分统计(支持日期筛选,已按部门、姓名排序)
             List<WorkKnowledgeBasePoint> employeeList = workKnowledgeBasePointDetailService.findAllEmployeePointStats(queryParam);
 
-            // 公共表头
-            List<String> metricHeaders = Arrays.asList(
-                "上传文档数量|uploadCount",
-                "贡献积分额度|createPoint",
-                "审核文档数量|auditCount",
-                "审核积分额度|auditPoint",
-                "阅读积分额度|readPoint",
-                "点赞积分额度|likePoint",
-                "已兑换积分额度|exchangePoint"
+            // 构建时间范围标识(如传入了日期筛选)
+            String dateRangeSuffix = "";
+            if (StringUtils.isNotBlank(queryParam.getBeginDate()) || StringUtils.isNotBlank(queryParam.getEndDate())) {
+                String startDate = StringUtils.isNotBlank(queryParam.getBeginDate()) ? queryParam.getBeginDate() : "不限";
+                String endDate = StringUtils.isNotBlank(queryParam.getEndDate()) ? queryParam.getEndDate() : "不限";
+                dateRangeSuffix = "(" + startDate + " 至 " + endDate + ")";
+            }
+
+            // 定义固定的7个业务指标列(第一行表头)
+            List<String> firstRowHeaders = Arrays.asList(
+                "上传文档数量",
+                "贡献积分额度",
+                "审核文档数量",
+                "审核积分额度",
+                "阅读积分额度",
+                "点赞积分额度",
+                "已兑换积分额度"
+            );
+            
+            // 获取知识库分类树结构(一级分类列表,用于第二行表头细分)
+            java.util.Map<String, List<String>> categoryTreeMap = treeService.getCategoryTreeForExport();
+            List<String> categoryNames = new ArrayList<>(categoryTreeMap.keySet());
+            
+            // 7个业务指标对应的数据字段key(与entity中的getter对应)
+            List<String> columnKeys = Arrays.asList(
+                "uploadCount", "createPoint", "auditCount", "auditPoint",
+                "readPoint", "likePoint", "exchangePoint"
             );
+            
+            // 构建第二行表头:每个固定列下包含"总量"+各一级分类
+            List<List<String>> secondRowHeaders = new ArrayList<>();
+            for (String columnHeader : firstRowHeaders) {
+                List<String> subHeaders = new ArrayList<>();
+                subHeaders.add("总量");
+                for (String categoryName : categoryNames) {
+                    subHeaders.add(categoryName);
+                }
+                secondRowHeaders.add(subHeaders);
+            }
+            
+            // 收集所有用户ID
+            List<String> userIds = employeeList.stream()
+                .map(emp -> emp.getUserId())
+                .filter(StringUtils::isNotBlank)
+                .collect(Collectors.toList());
+            
+            // ========== 分别查询7个业务指标的分类数据 ==========
+                        
+            // 1. 上传文档数量(按一级分类统计用户上传的文档数)
+            java.util.Map<String, java.util.Map<String, Integer>> uploadCountByCategory = new java.util.HashMap<>();
+            if (!userIds.isEmpty() && !categoryNames.isEmpty()) {
+                List<java.util.Map<String, Object>> results = 
+                    workKnowledgeBasePointDetailService.countUploadDocsByTreeNodeIds(categoryNames, userIds, queryParam.getBeginDate(), queryParam.getEndDate());
+                for (java.util.Map<String, Object> result : results) {
+                    String userId = (String) result.get("userId");
+                    String treeName = (String) result.get("treeName");
+                    Integer count = ((Number) result.get("docCount")).intValue();
+                    uploadCountByCategory.computeIfAbsent(userId, k -> new java.util.HashMap<>())
+                        .put(treeName, count);
+                }
+            }
+            
+            // 2. 贡献积分额度(change_type=1,创建积分)
+            java.util.Map<String, java.util.Map<String, Integer>> createPointByCategory = new java.util.HashMap<>();
+            if (!userIds.isEmpty() && !categoryNames.isEmpty()) {
+                List<java.util.Map<String, Object>> results = 
+                    workKnowledgeBasePointDetailService.sumPointsByTreeNodeIdsAndChangeType(categoryNames, userIds, 1, queryParam.getBeginDate(), queryParam.getEndDate());
+                for (java.util.Map<String, Object> result : results) {
+                    String userId = (String) result.get("userId");
+                    String treeName = (String) result.get("treeName");
+                    Integer point = ((Number) result.get("totalPoint")).intValue();
+                    createPointByCategory.computeIfAbsent(userId, k -> new java.util.HashMap<>())
+                        .put(treeName, point);
+                }
+            }
+            
+            // 3. 审核文档数量(按一级分类统计用户审核的文档数)
+            java.util.Map<String, java.util.Map<String, Integer>> auditCountByCategory = new java.util.HashMap<>();
+            if (!userIds.isEmpty() && !categoryNames.isEmpty()) {
+                List<java.util.Map<String, Object>> results = 
+                    workKnowledgeBasePointDetailService.countAuditDocsByTreeNodeIds(categoryNames, userIds, queryParam.getBeginDate(), queryParam.getEndDate());
+                for (java.util.Map<String, Object> result : results) {
+                    String userId = (String) result.get("userId");
+                    String treeName = (String) result.get("treeName");
+                    Integer count = ((Number) result.get("docCount")).intValue();
+                    auditCountByCategory.computeIfAbsent(userId, k -> new java.util.HashMap<>())
+                        .put(treeName, count);
+                }
+            }
+            
+            // 4. 审核积分额度(change_type=2,审核积分)
+            java.util.Map<String, java.util.Map<String, Integer>> auditPointByCategory = new java.util.HashMap<>();
+            if (!userIds.isEmpty() && !categoryNames.isEmpty()) {
+                List<java.util.Map<String, Object>> results = 
+                    workKnowledgeBasePointDetailService.sumPointsByTreeNodeIdsAndChangeType(categoryNames, userIds, 2, queryParam.getBeginDate(), queryParam.getEndDate());
+                for (java.util.Map<String, Object> result : results) {
+                    String userId = (String) result.get("userId");
+                    String treeName = (String) result.get("treeName");
+                    Integer point = ((Number) result.get("totalPoint")).intValue();
+                    auditPointByCategory.computeIfAbsent(userId, k -> new java.util.HashMap<>())
+                        .put(treeName, point);
+                }
+            }
+            
+            // 5. 阅读积分额度(change_type=3,阅读积分)
+            java.util.Map<String, java.util.Map<String, Integer>> readPointByCategory = new java.util.HashMap<>();
+            if (!userIds.isEmpty() && !categoryNames.isEmpty()) {
+                List<java.util.Map<String, Object>> results = 
+                    workKnowledgeBasePointDetailService.sumPointsByTreeNodeIdsAndChangeType(categoryNames, userIds, 3, queryParam.getBeginDate(), queryParam.getEndDate());
+                for (java.util.Map<String, Object> result : results) {
+                    String userId = (String) result.get("userId");
+                    String treeName = (String) result.get("treeName");
+                    Integer point = ((Number) result.get("totalPoint")).intValue();
+                    readPointByCategory.computeIfAbsent(userId, k -> new java.util.HashMap<>())
+                        .put(treeName, point);
+                }
+            }
+            
+            // 6. 点赞积分额度(change_type=4,点赞积分)
+            java.util.Map<String, java.util.Map<String, Integer>> likePointByCategory = new java.util.HashMap<>();
+            if (!userIds.isEmpty() && !categoryNames.isEmpty()) {
+                List<java.util.Map<String, Object>> results = 
+                    workKnowledgeBasePointDetailService.sumPointsByTreeNodeIdsAndChangeType(categoryNames, userIds, 4, queryParam.getBeginDate(), queryParam.getEndDate());
+                for (java.util.Map<String, Object> result : results) {
+                    String userId = (String) result.get("userId");
+                    String treeName = (String) result.get("treeName");
+                    Integer point = ((Number) result.get("totalPoint")).intValue();
+                    likePointByCategory.computeIfAbsent(userId, k -> new java.util.HashMap<>())
+                        .put(treeName, point);
+                }
+            }
+            
+            // 7. 已兑换积分额度(point<0,兑换积分)
+            java.util.Map<String, java.util.Map<String, Integer>> exchangePointByCategory = new java.util.HashMap<>();
+            if (!userIds.isEmpty() && !categoryNames.isEmpty()) {
+                List<java.util.Map<String, Object>> results = 
+                    workKnowledgeBasePointDetailService.sumExchangePointsByTreeNodeIds(categoryNames, userIds, queryParam.getBeginDate(), queryParam.getEndDate());
+                for (java.util.Map<String, Object> result : results) {
+                    String userId = (String) result.get("userId");
+                    String treeName = (String) result.get("treeName");
+                    Integer point = ((Number) result.get("totalPoint")).intValue();
+                    exchangePointByCategory.computeIfAbsent(userId, k -> new java.util.HashMap<>())
+                        .put(treeName, point);
+                }
+            }
 
-            ExportKnowledgeBaseExcel excel = new ExportKnowledgeBaseExcel();
+            // 业务指标字段与entity getter的映射
+            java.util.Map<String, String> columnGetterMap = new java.util.LinkedHashMap<>();
+            columnGetterMap.put("uploadCount", "getUploadCount");
+            columnGetterMap.put("createPoint", "getCreatePoint");
+            columnGetterMap.put("auditCount", "getAuditCount");
+            columnGetterMap.put("auditPoint", "getAuditPoint");
+            columnGetterMap.put("readPoint", "getReadPoint");
+            columnGetterMap.put("likePoint", "getLikePoint");
+            columnGetterMap.put("exchangePoint", "getExchangePoint");
 
-            // ========== 1. 公司级汇总 ==========
-            List<String> companyHeaders = new ArrayList<>();
-            companyHeaders.add("公司汇总|label");
-            companyHeaders.addAll(metricHeaders);
+            ExportKnowledgeBaseExcel excel = new ExportKnowledgeBaseExcel();
 
+            // ========== 1. 公司级汇总(双表头)==========
             List<Map<String, Object>> companyData = new ArrayList<>();
-            if (!employeeList.isEmpty()) {
-                Map<String, Object> companyRow = new LinkedHashMap<>();
-                companyRow.put("label", "公司汇总");
-                companyRow.put("uploadCount", employeeList.stream().mapToInt(e -> e.getUploadCount() != null ? e.getUploadCount() : 0).sum());
-                companyRow.put("createPoint", employeeList.stream().mapToInt(e -> e.getCreatePoint() != null ? e.getCreatePoint() : 0).sum());
-                companyRow.put("auditCount", employeeList.stream().mapToInt(e -> e.getAuditCount() != null ? e.getAuditCount() : 0).sum());
-                companyRow.put("auditPoint", employeeList.stream().mapToInt(e -> e.getAuditPoint() != null ? e.getAuditPoint() : 0).sum());
-                companyRow.put("readPoint", employeeList.stream().mapToInt(e -> e.getReadPoint() != null ? e.getReadPoint() : 0).sum());
-                companyRow.put("likePoint", employeeList.stream().mapToInt(e -> e.getLikePoint() != null ? e.getLikePoint() : 0).sum());
-                companyRow.put("exchangePoint", employeeList.stream().mapToInt(e -> e.getExchangePoint() != null ? e.getExchangePoint() : 0).sum());
-                companyData.add(companyRow);
+            Map<String, Object> companyRow = new LinkedHashMap<>();
+            companyRow.put("label", "公司汇总");
+            
+            for (int i = 0; i < firstRowHeaders.size(); i++) {
+                String columnKey = columnKeys.get(i);
+                List<String> subHeaders = secondRowHeaders.get(i);
+                
+                java.util.Map<String, java.util.Map<String, Integer>> dataSource;
+                switch (i) {
+                    case 0: dataSource = uploadCountByCategory; break;
+                    case 1: dataSource = createPointByCategory; break;
+                    case 2: dataSource = auditCountByCategory; break;
+                    case 3: dataSource = auditPointByCategory; break;
+                    case 4: dataSource = readPointByCategory; break;
+                    case 5: dataSource = likePointByCategory; break;
+                    case 6: dataSource = exchangePointByCategory; break;
+                    default: dataSource = new java.util.HashMap<>();
+                }
+                
+                for (String subHeader : subHeaders) {
+                    String dataKey = columnKey + "_" + subHeader;
+                    if ("总量".equals(subHeader)) {
+                        // 总量从 employeeList 累加
+                        int total = employeeList.stream().mapToInt(emp -> {
+                            try {
+                                java.lang.reflect.Method m = emp.getClass().getMethod(columnGetterMap.get(columnKey));
+                                Number val = (Number) m.invoke(emp);
+                                return val != null ? val.intValue() : 0;
+                            } catch (Exception e) { return 0; }
+                        }).sum();
+                        companyRow.put(dataKey, total);
+                    } else {
+                        // 分类数据从分类查询结果获取
+                        int categorySum = employeeList.stream()
+                            .mapToInt(emp -> {
+                                java.util.Map<String, Integer> points = dataSource.get(emp.getUserId());
+                                return points != null && points.containsKey(subHeader) ? points.get(subHeader) : 0;
+                            }).sum();
+                        companyRow.put(dataKey, categorySum);
+                    }
+                }
             }
-            excel.addSheet("公司级", "公司级积分统计汇总", companyHeaders, companyData);
-
-            // ========== 2. 部门级汇总 ==========
-            List<String> deptHeaders = new ArrayList<>();
-            deptHeaders.add("部门|officeName");
-            deptHeaders.addAll(metricHeaders);
+            companyData.add(companyRow);
+            
+            excel.addDoubleHeaderSheet("公司级", "公司级积分统计汇总" + dateRangeSuffix,
+                Arrays.asList("名称|label"), firstRowHeaders, secondRowHeaders, columnKeys, companyData);
 
-            // 按部门分组汇总
-            Map<String, Map<String, Object>> deptMap = new LinkedHashMap<>();
+            // ========== 2. 部门级汇总(双表头)==========
+            // 按部门分组
+            java.util.Map<String, List<WorkKnowledgeBasePoint>> deptMap = new LinkedHashMap<>();
             for (WorkKnowledgeBasePoint emp : employeeList) {
                 String dept = emp.getOfficeName() != null ? emp.getOfficeName() : "未分配部门";
-                Map<String, Object> deptRow = deptMap.computeIfAbsent(dept, k -> {
-                    Map<String, Object> m = new LinkedHashMap<>();
-                    m.put("officeName", k);
-                    m.put("uploadCount", 0);
-                    m.put("createPoint", 0);
-                    m.put("auditCount", 0);
-                    m.put("auditPoint", 0);
-                    m.put("readPoint", 0);
-                    m.put("likePoint", 0);
-                    m.put("exchangePoint", 0);
-                    return m;
-                });
-                deptRow.put("uploadCount", (int) deptRow.get("uploadCount") + (emp.getUploadCount() != null ? emp.getUploadCount() : 0));
-                deptRow.put("createPoint", (int) deptRow.get("createPoint") + (emp.getCreatePoint() != null ? emp.getCreatePoint() : 0));
-                deptRow.put("auditCount", (int) deptRow.get("auditCount") + (emp.getAuditCount() != null ? emp.getAuditCount() : 0));
-                deptRow.put("auditPoint", (int) deptRow.get("auditPoint") + (emp.getAuditPoint() != null ? emp.getAuditPoint() : 0));
-                deptRow.put("readPoint", (int) deptRow.get("readPoint") + (emp.getReadPoint() != null ? emp.getReadPoint() : 0));
-                deptRow.put("likePoint", (int) deptRow.get("likePoint") + (emp.getLikePoint() != null ? emp.getLikePoint() : 0));
-                deptRow.put("exchangePoint", (int) deptRow.get("exchangePoint") + (emp.getExchangePoint() != null ? emp.getExchangePoint() : 0));
+                deptMap.computeIfAbsent(dept, k -> new ArrayList<>()).add(emp);
             }
-            excel.addSheet("部门级", "部门级积分统计汇总", deptHeaders, new ArrayList<>(deptMap.values()));
-
-            // ========== 3. 员工级明细 ==========
-            List<String> empHeaders = new ArrayList<>();
-            empHeaders.add("部门|officeName");
-            empHeaders.add("姓名|userName");
-            empHeaders.addAll(metricHeaders);
+            
+            List<Map<String, Object>> deptData = new ArrayList<>();
+            for (java.util.Map.Entry<String, List<WorkKnowledgeBasePoint>> entry : deptMap.entrySet()) {
+                String deptName = entry.getKey();
+                List<WorkKnowledgeBasePoint> deptEmps = entry.getValue();
+                
+                Map<String, Object> row = new LinkedHashMap<>();
+                row.put("officeName", deptName);
+                
+                for (int i = 0; i < firstRowHeaders.size(); i++) {
+                    String columnKey = columnKeys.get(i);
+                    List<String> subHeaders = secondRowHeaders.get(i);
+                    
+                    java.util.Map<String, java.util.Map<String, Integer>> dataSource;
+                    switch (i) {
+                        case 0: dataSource = uploadCountByCategory; break;
+                        case 1: dataSource = createPointByCategory; break;
+                        case 2: dataSource = auditCountByCategory; break;
+                        case 3: dataSource = auditPointByCategory; break;
+                        case 4: dataSource = readPointByCategory; break;
+                        case 5: dataSource = likePointByCategory; break;
+                        case 6: dataSource = exchangePointByCategory; break;
+                        default: dataSource = new java.util.HashMap<>();
+                    }
+                    
+                    for (String subHeader : subHeaders) {
+                        String dataKey = columnKey + "_" + subHeader;
+                        if ("总量".equals(subHeader)) {
+                            int total = deptEmps.stream().mapToInt(emp -> {
+                                try {
+                                    java.lang.reflect.Method m = emp.getClass().getMethod(columnGetterMap.get(columnKey));
+                                    Number val = (Number) m.invoke(emp);
+                                    return val != null ? val.intValue() : 0;
+                                } catch (Exception e) { return 0; }
+                            }).sum();
+                            row.put(dataKey, total);
+                        } else {
+                            int categorySum = deptEmps.stream()
+                                .mapToInt(emp -> {
+                                    java.util.Map<String, Integer> points = dataSource.get(emp.getUserId());
+                                    return points != null && points.containsKey(subHeader) ? points.get(subHeader) : 0;
+                                }).sum();
+                            row.put(dataKey, categorySum);
+                        }
+                    }
+                }
+                deptData.add(row);
+            }
+            
+            excel.addDoubleHeaderSheet("部门级", "部门级积分统计汇总" + dateRangeSuffix,
+                Arrays.asList("部门名称|officeName"), firstRowHeaders, secondRowHeaders, columnKeys, deptData);
 
+            // ========== 3. 员工级明细(双表头)==========
             List<Map<String, Object>> empData = new ArrayList<>();
             for (WorkKnowledgeBasePoint emp : employeeList) {
                 Map<String, Object> row = new LinkedHashMap<>();
                 row.put("officeName", emp.getOfficeName() != null ? emp.getOfficeName() : "未分配部门");
                 row.put("userName", emp.getUserName() != null ? emp.getUserName() : "");
-                row.put("uploadCount", emp.getUploadCount() != null ? emp.getUploadCount() : 0);
-                row.put("createPoint", emp.getCreatePoint() != null ? emp.getCreatePoint() : 0);
-                row.put("auditCount", emp.getAuditCount() != null ? emp.getAuditCount() : 0);
-                row.put("auditPoint", emp.getAuditPoint() != null ? emp.getAuditPoint() : 0);
-                row.put("readPoint", emp.getReadPoint() != null ? emp.getReadPoint() : 0);
-                row.put("likePoint", emp.getLikePoint() != null ? emp.getLikePoint() : 0);
-                row.put("exchangePoint", emp.getExchangePoint() != null ? emp.getExchangePoint() : 0);
+                
+                for (int i = 0; i < firstRowHeaders.size(); i++) {
+                    String columnKey = columnKeys.get(i);
+                    List<String> subHeaders = secondRowHeaders.get(i);
+                    
+                    java.util.Map<String, java.util.Map<String, Integer>> dataSource;
+                    switch (i) {
+                        case 0: dataSource = uploadCountByCategory; break;
+                        case 1: dataSource = createPointByCategory; break;
+                        case 2: dataSource = auditCountByCategory; break;
+                        case 3: dataSource = auditPointByCategory; break;
+                        case 4: dataSource = readPointByCategory; break;
+                        case 5: dataSource = likePointByCategory; break;
+                        case 6: dataSource = exchangePointByCategory; break;
+                        default: dataSource = new java.util.HashMap<>();
+                    }
+                    
+                    for (String subHeader : subHeaders) {
+                        String dataKey = columnKey + "_" + subHeader;
+                        if ("总量".equals(subHeader)) {
+                            try {
+                                java.lang.reflect.Method m = emp.getClass().getMethod(columnGetterMap.get(columnKey));
+                                Number val = (Number) m.invoke(emp);
+                                row.put(dataKey, val != null ? val.intValue() : 0);
+                            } catch (Exception e) {
+                                row.put(dataKey, 0);
+                            }
+                        } else {
+                            java.util.Map<String, Integer> points = dataSource.get(emp.getUserId());
+                            row.put(dataKey, points != null && points.containsKey(subHeader) ? points.get(subHeader) : 0);
+                        }
+                    }
+                }
                 empData.add(row);
             }
-            excel.addSheet("员工级", "员工级积分统计明细", empHeaders, empData);
+            
+            excel.addDoubleHeaderSheet("员工级", "员工级积分统计明细" + dateRangeSuffix,
+                Arrays.asList("部门名称|officeName", "用户姓名|userName"), firstRowHeaders, secondRowHeaders, columnKeys, empData);
 
-            String fileName = "积分统计三级导出_" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+            String fileName = "知识库统计表_" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
             excel.write(response, fileName);
             excel.dispose();
 

+ 145 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml

@@ -526,4 +526,149 @@
         ORDER BY CASE WHEN u.user_type = '1' THEN '系统' ELSE o.name END, u.name
     </select>
 
+    <!-- 根据知识库一级分类名称和用户ID列表,按父级分类统计积分总和(用于双表头导出) -->
+    <select id="sumPointsByTreeNodeIds" resultType="java.util.HashMap">
+        SELECT
+            d.user_id AS userId,
+            p.tree_name AS treeName,
+            SUM(d.point) AS totalPoint
+        FROM work_knowledge_base_point_detail d
+        INNER JOIN work_knowledge_base_share_info s ON s.id = d.share_id AND s.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info t ON t.id = s.tree_node_id AND t.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info p ON p.id = t.parent_id AND p.del_flag = '0'
+        WHERE d.del_flag = '0'
+          AND d.point &gt; 0
+          AND p.tree_name IN
+          <foreach collection="treeNames" item="treeName" open="(" separator="," close=")">
+              #{treeName}
+          </foreach>
+          AND d.user_id IN
+          <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+              #{userId}
+          </foreach>
+          <if test="beginDate != null and beginDate != ''">
+              AND d.create_date &gt;= #{beginDate}
+          </if>
+          <if test="endDate != null and endDate != ''">
+              AND d.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+          </if>
+        GROUP BY d.user_id, p.tree_name
+    </select>
+
+    <!-- 按change_type和一级分类统计积分总和 -->
+    <select id="sumPointsByTreeNodeIdsAndChangeType" resultType="java.util.HashMap">
+        SELECT
+            d.user_id AS userId,
+            p.tree_name AS treeName,
+            SUM(d.point) AS totalPoint
+        FROM work_knowledge_base_point_detail d
+        INNER JOIN work_knowledge_base_share_info s ON s.id = d.share_id AND s.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info t ON t.id = s.tree_node_id AND t.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info p ON p.id = t.parent_id AND p.del_flag = '0'
+        WHERE d.del_flag = '0'
+          AND d.change_type = #{changeType}
+          AND d.point &gt; 0
+          AND p.tree_name IN
+          <foreach collection="treeNames" item="treeName" open="(" separator="," close=")">
+              #{treeName}
+          </foreach>
+          AND d.user_id IN
+          <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+              #{userId}
+          </foreach>
+          <if test="beginDate != null and beginDate != ''">
+              AND d.create_date &gt;= #{beginDate}
+          </if>
+          <if test="endDate != null and endDate != ''">
+              AND d.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+          </if>
+        GROUP BY d.user_id, p.tree_name
+    </select>
+
+    <!-- 按一级分类统计兑换积分总和(point<0) -->
+    <select id="sumExchangePointsByTreeNodeIds" resultType="java.util.HashMap">
+        SELECT
+            d.user_id AS userId,
+            p.tree_name AS treeName,
+            ABS(SUM(d.point)) AS totalPoint
+        FROM work_knowledge_base_point_detail d
+        INNER JOIN work_knowledge_base_share_info s ON s.id = d.share_id AND s.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info t ON t.id = s.tree_node_id AND t.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info p ON p.id = t.parent_id AND p.del_flag = '0'
+        WHERE d.del_flag = '0'
+          AND d.point &lt; 0
+          AND p.tree_name IN
+          <foreach collection="treeNames" item="treeName" open="(" separator="," close=")">
+              #{treeName}
+          </foreach>
+          AND d.user_id IN
+          <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+              #{userId}
+          </foreach>
+          <if test="beginDate != null and beginDate != ''">
+              AND d.create_date &gt;= #{beginDate}
+          </if>
+          <if test="endDate != null and endDate != ''">
+              AND d.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+          </if>
+        GROUP BY d.user_id, p.tree_name
+    </select>
+
+    <!-- 按一级分类统计上传文档数量 -->
+    <select id="countUploadDocsByTreeNodeIds" resultType="java.util.HashMap">
+        SELECT
+            COALESCE(s.submit_audit_user_id, s.create_by) AS userId,
+            p.tree_name AS treeName,
+            COUNT(DISTINCT s.id) AS docCount
+        FROM work_knowledge_base_share_info s
+        INNER JOIN work_knowledge_base_tree_info t ON t.id = s.tree_node_id AND t.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info p ON p.id = t.parent_id AND p.del_flag = '0'
+        WHERE s.del_flag = '0'
+          AND s.audit_status != '0'
+          AND p.tree_name IN
+          <foreach collection="treeNames" item="treeName" open="(" separator="," close=")">
+              #{treeName}
+          </foreach>
+          AND COALESCE(s.submit_audit_user_id, s.create_by) IN
+          <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+              #{userId}
+          </foreach>
+          <if test="beginDate != null and beginDate != ''">
+              AND s.create_date &gt;= #{beginDate}
+          </if>
+          <if test="endDate != null and endDate != ''">
+              AND s.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+          </if>
+        GROUP BY COALESCE(s.submit_audit_user_id, s.create_by), p.tree_name
+    </select>
+
+    <!-- 按一级分类统计审核文档数量 -->
+    <select id="countAuditDocsByTreeNodeIds" resultType="java.util.HashMap">
+        SELECT
+            d.user_id AS userId,
+            p.tree_name AS treeName,
+            COUNT(DISTINCT d.share_id) AS docCount
+        FROM work_knowledge_base_point_detail d
+        INNER JOIN work_knowledge_base_share_info s ON s.id = d.share_id AND s.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info t ON t.id = s.tree_node_id AND t.del_flag = '0'
+        INNER JOIN work_knowledge_base_tree_info p ON p.id = t.parent_id AND p.del_flag = '0'
+        WHERE d.del_flag = '0'
+          AND d.change_type = 2
+          AND p.tree_name IN
+          <foreach collection="treeNames" item="treeName" open="(" separator="," close=")">
+              #{treeName}
+          </foreach>
+          AND d.user_id IN
+          <foreach collection="userIds" item="userId" open="(" separator="," close=")">
+              #{userId}
+          </foreach>
+          <if test="beginDate != null and beginDate != ''">
+              AND d.create_date &gt;= #{beginDate}
+          </if>
+          <if test="endDate != null and endDate != ''">
+              AND d.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+          </if>
+        GROUP BY d.user_id, p.tree_name
+    </select>
+
 </mapper>

+ 13 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseTreeInfoDao.xml

@@ -188,4 +188,17 @@
         ORDER BY a.parent_ids ASC, a.sort ASC
     </select>
 
+    <!-- 查询所有一级分类及其直接子节点名称(返回扁平结构,Java层分组) -->
+    <select id="findFirstLevelCategoriesWithChildren" resultType="java.util.HashMap">
+        SELECT
+            parent.tree_name AS parentName,
+            child.tree_name  AS childName
+        FROM work_knowledge_base_tree_info parent
+                 LEFT JOIN work_knowledge_base_tree_info child ON child.parent_id = parent.id
+                    AND child.del_flag = '0'
+        WHERE parent.parent_id = '0'
+          AND parent.del_flag = '0'
+        ORDER BY parent.sort ASC, child.sort ASC
+    </select>
+
 </mapper>