Bläddra i källkod

知识库 查看兑换以及积分情况数据信息

徐滕 2 veckor sedan
förälder
incheckning
b4d18fa79b

+ 160 - 0
src/main/java/com/jeeplus/common/utils/excel/ExportKnowledgeBaseAuditExcel.java

@@ -0,0 +1,160 @@
+package com.jeeplus.common.utils.excel;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.XSSFColor;
+import org.apache.poi.xssf.usermodel.XSSFCellStyle;
+
+import java.util.*;
+
+/**
+ * 知识库含审核信息Excel导出工具类(继承自ExportKnowledgeBaseExcel)
+ * 在原有导出基础上增加审核列的专用样式(淡绿底色)和列宽控制。
+ * 仅用于 exportAllTabsWithAudit 接口,不影响原 ExportKnowledgeBaseExcel 的行为。
+ */
+public class ExportKnowledgeBaseAuditExcel extends ExportKnowledgeBaseExcel {
+
+	/**
+	 * 覆写样式创建:在父类基础样式上追加审核列专用样式(淡绿底色)
+	 */
+	@Override
+	protected Map<String, CellStyle> createStyles(Workbook wb) {
+		Map<String, CellStyle> styles = super.createStyles(wb);
+
+		// 审核列样式(淡绿色底色,用于审核人/审核时间/审核间隔列)
+		XSSFColor auditorBgColor = new XSSFColor(new byte[]{(byte)220, (byte)245, (byte)220});
+
+		CellStyle auditorHeaderStyle = wb.createCellStyle();
+		auditorHeaderStyle.setAlignment(HorizontalAlignment.CENTER);
+		auditorHeaderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+		auditorHeaderStyle.setBorderBottom(BorderStyle.THIN);
+		auditorHeaderStyle.setBorderLeft(BorderStyle.THIN);
+		auditorHeaderStyle.setBorderRight(BorderStyle.THIN);
+		auditorHeaderStyle.setBorderTop(BorderStyle.THIN);
+		((XSSFCellStyle) auditorHeaderStyle).setFillForegroundColor(auditorBgColor);
+		auditorHeaderStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+		Font auditorHeaderFont = wb.createFont();
+		auditorHeaderFont.setFontName("宋体");
+		auditorHeaderFont.setFontHeightInPoints((short) 11);
+		auditorHeaderFont.setBold(true);
+		auditorHeaderStyle.setFont(auditorHeaderFont);
+		styles.put("auditor_header", auditorHeaderStyle);
+
+		CellStyle auditorStringStyle = wb.createCellStyle();
+		auditorStringStyle.setAlignment(HorizontalAlignment.CENTER);
+		auditorStringStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+		auditorStringStyle.setBorderBottom(BorderStyle.THIN);
+		auditorStringStyle.setBorderLeft(BorderStyle.THIN);
+		auditorStringStyle.setBorderRight(BorderStyle.THIN);
+		auditorStringStyle.setBorderTop(BorderStyle.THIN);
+		((XSSFCellStyle) auditorStringStyle).setFillForegroundColor(auditorBgColor);
+		auditorStringStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+		Font auditorFont = wb.createFont();
+		auditorFont.setFontName("宋体");
+		auditorFont.setFontHeightInPoints((short) 11);
+		auditorStringStyle.setFont(auditorFont);
+		styles.put("auditor_data_string", auditorStringStyle);
+
+		CellStyle auditorNumberStyle = wb.createCellStyle();
+		auditorNumberStyle.setAlignment(HorizontalAlignment.RIGHT);
+		auditorNumberStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+		auditorNumberStyle.setBorderBottom(BorderStyle.THIN);
+		auditorNumberStyle.setBorderLeft(BorderStyle.THIN);
+		auditorNumberStyle.setBorderRight(BorderStyle.THIN);
+		auditorNumberStyle.setBorderTop(BorderStyle.THIN);
+		((XSSFCellStyle) auditorNumberStyle).setFillForegroundColor(auditorBgColor);
+		auditorNumberStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+		Font auditorNumFont = wb.createFont();
+		auditorNumFont.setFontName("宋体");
+		auditorNumFont.setFontHeightInPoints((short) 11);
+		auditorNumberStyle.setFont(auditorNumFont);
+		styles.put("auditor_data_number", auditorNumberStyle);
+
+		return styles;
+	}
+
+	/**
+	 * 覆写Map模式Sheet内容创建:增加审核列的宽度控制和专用样式
+	 */
+	@Override
+	protected void createMapSheetContent(MapSheetData sheetData) {
+		Workbook workbook = getWorkbook();
+		Sheet sheet = workbook.createSheet(sheetData.sheetName);
+		Map<String, CellStyle> styles = createStyles(workbook);
+		List<String> headers = sheetData.headers;
+		int rownum = 0;
+
+		// 创建标题行
+		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, headers.size() - 1));
+		}
+
+		// 创建表头行
+		Row headerRow = sheet.createRow(rownum++);
+		headerRow.setHeightInPoints(18);
+		for (int i = 0; i < headers.size(); i++) {
+			String header = headers.get(i);
+			String displayHeader = header.contains("|") ? header.split("\\|")[0] : header;
+			String mapKey = header.contains("|") ? header.split("\\|")[1] : header;
+			boolean isAuditorCol = mapKey.startsWith("auditor");
+			int width = 0;
+			for (char c : displayHeader.toCharArray()) {
+				width += (c > 127) ? 2 : 1;
+			}
+			width = Math.max(width, 10);
+			width = Math.min(width, 50);
+			if (i == 0) {
+				width = Math.min(width, 5);
+			}
+			// 审核列自定义宽度
+			if (isAuditorCol) {
+				if (mapKey.endsWith("Name")) {
+					width = 6;
+				} else if (mapKey.endsWith("Gap")) {
+					width = 8;
+				} else {
+					width = 12;
+				}
+			}
+			sheet.setColumnWidth(i, width * 256 * 2);
+
+			Cell headerCell = headerRow.createCell(i);
+			headerCell.setCellStyle(isAuditorCol ? styles.get("auditor_header") : styles.get("prefix_header"));
+			headerCell.setCellValue(displayHeader);
+		}
+
+		// 填充数据行
+		if (sheetData.dataList != null && !sheetData.dataList.isEmpty()) {
+			for (Map<String, Object> dataMap : sheetData.dataList) {
+				Row dataRow = sheet.createRow(rownum++);
+				dataRow.setHeightInPoints(16);
+				for (int i = 0; i < headers.size(); i++) {
+					String header = headers.get(i);
+					String mapKey = header.contains("|") ? header.split("\\|")[1] : header;
+					boolean isAuditorCol = mapKey.startsWith("auditor");
+					Object value = dataMap.get(mapKey);
+
+					Cell cell = dataRow.createCell(i);
+					if (isAuditorCol) {
+						if (value instanceof Number) {
+							cell.setCellStyle(styles.get("auditor_data_number"));
+						} else {
+							cell.setCellStyle(styles.get("auditor_data_string"));
+						}
+					} else if (value instanceof Number) {
+						cell.setCellStyle(styles.get("data_number"));
+					} else {
+						cell.setCellStyle(styles.get("data_string"));
+					}
+					setMapCellValue(cell, value);
+				}
+			}
+		}
+	}
+}

+ 11 - 4
src/main/java/com/jeeplus/common/utils/excel/ExportKnowledgeBaseExcel.java

@@ -33,7 +33,7 @@ public class ExportKnowledgeBaseExcel {
 	/**
 	 * 内部类:存储单个Sheet的数据(Map模式)
 	 */
-	private static class MapSheetData {
+	protected static class MapSheetData {
 		String sheetName;
 		String title;
 		List<String> headers;
@@ -157,7 +157,7 @@ public class ExportKnowledgeBaseExcel {
 	/**
 	 * 创建单元格样式
 	 */
-	private Map<String, CellStyle> createStyles(Workbook wb) {
+	protected Map<String, CellStyle> createStyles(Workbook wb) {
 		Map<String, CellStyle> styles = new HashMap<>();
 
 		// 标题样式
@@ -369,7 +369,7 @@ public class ExportKnowledgeBaseExcel {
 	 * 创建Map模式Sheet的内容
 	 * 表头格式为 "中文名|mapKey":表头行只显示中文名,数据按mapKey从Map中取值
 	 */
-	private void createMapSheetContent(MapSheetData sheetData) {
+	protected void createMapSheetContent(MapSheetData sheetData) {
 		Sheet sheet = workbook.createSheet(sheetData.sheetName);
 		Map<String, CellStyle> styles = createStyles(workbook);
 		List<String> headers = sheetData.headers;
@@ -435,7 +435,7 @@ public class ExportKnowledgeBaseExcel {
 	/**
 	 * 设置Map模式单元格值
 	 */
-	private void setMapCellValue(Cell cell, Object value) {
+	protected void setMapCellValue(Cell cell, Object value) {
 		if (value == null) {
 			cell.setCellValue("");
 			return;
@@ -851,6 +851,13 @@ public class ExportKnowledgeBaseExcel {
 	}
 
 	/**
+	 * 获取工作簿对象(供子类使用)
+	 */
+	protected Workbook getWorkbook() {
+		return workbook;
+	}
+
+	/**
 	 * 清理临时文件(避免内存泄漏)
 	 */
 	public void dispose() {

+ 16 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseAuditRecordDao.java

@@ -7,6 +7,8 @@ import org.apache.ibatis.annotations.Param;
 
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
+import java.util.Map;
 
 /**
  * 知识库审核记录 DAO
@@ -65,4 +67,18 @@ public interface WorkKnowledgeBaseAuditRecordDao extends CrudDao<WorkKnowledgeBa
      * 根据文件ID逻辑删除所有审核记录(保留方法,如需作废可调用)
      */
     void logicDeleteByFileId(String fileId);
+
+    // ======================== 临时导出用(后期可删除) ========================
+
+    /**
+     * 批量查询多个文件的审核记录(含审核人姓名,按file_id分组、audit_time正序)
+     * 用于 exportAllTabsWithAudit 导出,后期生产环境可删除
+     */
+    List<WorkKnowledgeBaseAuditRecord> findByFileIds(@Param("fileIds") List<String> fileIds);
+
+    /**
+     * 查询导出用审核数据(LEFT JOIN 临时表获取 time_gap_seconds)
+     * 用于 exportAllTabsWithAudit 导出,后期生产环境可删除
+     */
+    List<Map<String, Object>> findAuditExportData(@Param("fileIds") List<String> fileIds);
 }

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

@@ -48,4 +48,10 @@ public interface WorkKnowledgeBaseLikeRecordDao extends CrudDao<WorkKnowledgeBas
      * 查询所有已被点赞过(status=1)的文档 ID(去重)
      */
     List<String> findAllDistinctFileIds();
+
+    /**
+     * 按月份统计点赞数量(有效点赞 status=1)
+     */
+    List<Map<String, Object>> findMonthlyLikeStats(@Param("beginDate") String beginDate,
+                                                    @Param("endDate") String endDate);
 }

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

@@ -50,4 +50,10 @@ public interface WorkKnowledgeBaseReadRecordDao extends CrudDao<WorkKnowledgeBas
      * 查询所有已被阅读过的文档 ID(去重)
      */
     List<String> findAllDistinctFileIds();
+
+    /**
+     * 按月份统计阅读数量(去重用户数)
+     */
+    List<Map<String, Object>> findMonthlyReadStats(@Param("beginDate") String beginDate,
+                                                    @Param("endDate") String endDate);
 }

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

@@ -91,4 +91,10 @@ public interface WorkKnowledgeBaseShareInfoDao extends CrudDao<WorkKnowledgeBase
      * @return 文档 ID 列表
      */
     List<String> findIdsByCreateBy(@org.apache.ibatis.annotations.Param("userId") String userId);
+
+    /**
+     * 按月份统计上传数量
+     */
+    List<Map<String, Object>> findMonthlyUploadStats(@org.apache.ibatis.annotations.Param("beginDate") String beginDate,
+                                                      @org.apache.ibatis.annotations.Param("endDate") String endDate);
 }

+ 23 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseAuditRecordService.java

@@ -10,6 +10,7 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 知识库审核记录 Service
@@ -61,4 +62,26 @@ public class WorkKnowledgeBaseAuditRecordService extends CrudService<WorkKnowled
         record.preInsert();
         auditRecordDao.insert(record);
     }
+
+    // ======================== 临时导出用(后期可删除) ========================
+
+    /**
+     * 批量查询多个文件的审核记录(用于 exportAllTabsWithAudit 导出)
+     */
+    public List<WorkKnowledgeBaseAuditRecord> findByFileIds(List<String> fileIds) {
+        if (fileIds == null || fileIds.isEmpty()) {
+            return new java.util.ArrayList<>();
+        }
+        return auditRecordDao.findByFileIds(fileIds);
+    }
+
+    /**
+     * 查询导出用审核数据(含临时表 time_gap_seconds,用于 exportAllTabsWithAudit 导出)
+     */
+    public List<Map<String, Object>> findAuditExportData(List<String> fileIds) {
+        if (fileIds == null || fileIds.isEmpty()) {
+            return new java.util.ArrayList<>();
+        }
+        return auditRecordDao.findAuditExportData(fileIds);
+    }
 }

+ 81 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java

@@ -142,6 +142,12 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     @Autowired
     private WorkKnowledgeBaseShareLinkDao shareLinkDao;
 
+    @Autowired
+    private WorkKnowledgeBaseLikeRecordDao likeRecordDao;
+
+    @Autowired
+    private WorkKnowledgeBaseReadRecordDao readRecordDao;
+
     /**
      * 根据文件id加载附件列表(编辑表单回显用)
      */
@@ -911,6 +917,7 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
                 Workattachment firstAttach = attList.get(0);
                 map.put("fileUrl", firstAttach.getUrl());
                 map.put("fileName", firstAttach.getAttachmentName());
+                map.put("attachmentCreateDate", firstAttach.getCreateDate());
                 map.put("attachmentCount", attList.size());
                 StringBuilder namesBuilder = new StringBuilder();
                 for (int i = 0; i < attList.size(); i++) {
@@ -3180,4 +3187,78 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     public List<WorkKnowledgeBasePointDetail> getPointDetailList(WorkKnowledgeBasePointDetail query) {
         return pointDetailDao.findList(query);
     }
+
+    /**
+     * 月度汇总统计:按月份合并上传、点赞、阅读数量
+     * @param beginMonth 开始月份(如 "2026-07")
+     * @param endMonth 结束月份(如 "2026-09")
+     * @return 按月份分组的汇总列表
+     */
+    public List<Map<String, Object>> getMonthlyStats(String beginMonth, String endMonth) {
+        // 转换为日期范围:beginDate = "2026-07-01 00:00:00", endDate = "2026-10-01 00:00:00"
+        String beginDate = beginMonth + "-01 00:00:00";
+        // endMonth 的下个月1号
+        String[] endParts = endMonth.split("-");
+        int endYear = Integer.parseInt(endParts[0]);
+        int endMonthNum = Integer.parseInt(endParts[1]);
+        endMonthNum++;
+        if (endMonthNum > 12) {
+            endMonthNum = 1;
+            endYear++;
+        }
+        String endDate = String.format("%04d-%02d-01 00:00:00", endYear, endMonthNum);
+
+        // 分别查询三项统计
+        List<Map<String, Object>> uploadStats = dao.findMonthlyUploadStats(beginDate, endDate);
+        List<Map<String, Object>> likeStats = likeRecordDao.findMonthlyLikeStats(beginDate, endDate);
+        List<Map<String, Object>> readStats = readRecordDao.findMonthlyReadStats(beginDate, endDate);
+
+        // 按月份合并
+        Map<String, Map<String, Object>> merged = new java.util.LinkedHashMap<>();
+
+        // 生成所有月份(确保没有数据的月份也显示为0)
+        String[] bmParts = beginMonth.split("-");
+        int curYear = Integer.parseInt(bmParts[0]);
+        int curMonth = Integer.parseInt(bmParts[1]);
+        int eYear = Integer.parseInt(endParts[0]);
+        int eMonth = Integer.parseInt(endParts[1]);
+        while (curYear < eYear || (curYear == eYear && curMonth <= eMonth)) {
+            String monthKey = String.format("%04d-%02d", curYear, curMonth);
+            Map<String, Object> row = new HashMap<>();
+            row.put("month", monthKey);
+            row.put("uploadCount", 0);
+            row.put("likeCount", 0);
+            row.put("readCount", 0);
+            merged.put(monthKey, row);
+            curMonth++;
+            if (curMonth > 12) {
+                curMonth = 1;
+                curYear++;
+            }
+        }
+
+        // 填充上传数据
+        for (Map<String, Object> row : uploadStats) {
+            String month = row.get("month") != null ? row.get("month").toString() : "";
+            if (merged.containsKey(month)) {
+                merged.get(month).put("uploadCount", row.get("uploadCount"));
+            }
+        }
+        // 填充点赞数据
+        for (Map<String, Object> row : likeStats) {
+            String month = row.get("month") != null ? row.get("month").toString() : "";
+            if (merged.containsKey(month)) {
+                merged.get(month).put("likeCount", row.get("likeCount"));
+            }
+        }
+        // 填充阅读数据
+        for (Map<String, Object> row : readStats) {
+            String month = row.get("month") != null ? row.get("month").toString() : "";
+            if (merged.containsKey(month)) {
+                merged.get(month).put("readCount", row.get("readCount"));
+            }
+        }
+
+        return new ArrayList<>(merged.values());
+    }
 }

+ 292 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBaseShareController.java

@@ -7,10 +7,12 @@ import com.jeeplus.common.utils.DateUtils;
 import com.jeeplus.common.utils.StringUtils;
 import com.jeeplus.common.utils.excel.ExportExcel;
 import com.jeeplus.common.utils.excel.ExportKnowledgeBaseExcel;
+import com.jeeplus.common.utils.excel.ExportKnowledgeBaseAuditExcel;
 import com.jeeplus.common.utils.excel.ImportExcel;
 import com.jeeplus.common.utils.excel.utils.FileMatcher;
 import com.jeeplus.common.web.BaseController;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.*;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBaseAuditRecordService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointRuleService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBaseReadLikeService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBaseShareService;
@@ -29,7 +31,9 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import java.io.OutputStream;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -63,6 +67,9 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     @Autowired
     private WorkKnowledgeBaseReadLikeService readLikeService;
 
+    @Autowired
+    private WorkKnowledgeBaseAuditRecordService auditRecordService;
+
     @ModelAttribute
     public WorkKnowledgeBaseShareInfo get(@RequestParam(required = false) String id) {
         WorkKnowledgeBaseShareInfo entity = null;
@@ -1161,6 +1168,291 @@ public class WorkKnowledgeBaseShareController extends BaseController {
         }
     }
 
+    // ======================== 临时导出用(含审核信息,后期可删除) ========================
+
+    /**
+     * 导出所有页签(含审核人、审核时间、审核间隔)
+     * 临时方法,后期生产环境不需要可直接删除
+     */
+    @RequiresPermissions("workKnowledgeBase:share:export")
+    @RequestMapping(value = "exportAllTabsWithAudit")
+    public void exportAllTabsWithAudit(WorkKnowledgeBaseShareInfo shareInfo,
+                                        HttpServletRequest request, HttpServletResponse response) {
+        try {
+            // 获取所有根节点(parent_id='0')
+            List<WorkKnowledgeBaseTreeInfo> rootNodes = shareService.findRootNodes();
+            if (rootNodes == null || rootNodes.isEmpty()) {
+                throw new RuntimeException("没有找到知识库根节点");
+            }
+
+            // 收集固定筛选参数
+            String name = request.getParameter("name");
+            String beginDate = request.getParameter("beginDate");
+            String endDate = request.getParameter("endDate");
+
+            // 加载所有树节点,构建 nodeId -> treeName 映射
+            Map<String, String> nodeNameMap = new HashMap<>();
+            for (WorkKnowledgeBaseTreeInfo rn : rootNodes) {
+                nodeNameMap.put(rn.getId(), rn.getTreeName());
+                List<WorkKnowledgeBaseTreeInfo> descendants = shareService.findAllDescendantsByRootId(rn.getId());
+                if (descendants != null) {
+                    for (WorkKnowledgeBaseTreeInfo n : descendants) {
+                        nodeNameMap.put(n.getId(), n.getTreeName());
+                    }
+                }
+            }
+
+            ExportKnowledgeBaseAuditExcel excel = new ExportKnowledgeBaseAuditExcel();
+            Set<String> usedSheetNames = new HashSet<>();
+            List<Map<String, Object>> allExportData = new ArrayList<>(); // 汇总所有分类数据,用于最后的统计页签
+
+            // 为每个根节点创建一个Sheet
+            for (WorkKnowledgeBaseTreeInfo rootNode : rootNodes) {
+                String rootId = rootNode.getId();
+
+                // 获取该根节点配置的动态字段
+                List<WorkKnowledgeBaseDynamicInfo> rootDynamicFields = shareService.findDynamicFields(rootId);
+                if (rootDynamicFields == null) {
+                    rootDynamicFields = new ArrayList<>();
+                }
+
+                // 收集该根节点的动态字段筛选条件
+                Map<String, String> dynamicConditions = new HashMap<>();
+                for (WorkKnowledgeBaseDynamicInfo field : rootDynamicFields) {
+                    String paramValue = request.getParameter("dynamic_" + field.getId());
+                    if (StringUtils.isNotBlank(paramValue)) {
+                        dynamicConditions.put(field.getId(), paramValue.trim());
+                    }
+                }
+
+                // 构建导出列:序号 + 固定列 + 动态列 + 所属子节点 + 审核信息(最多3位审核人)
+                List<String> exportHeaders = new ArrayList<>();
+                exportHeaders.add("序号|_rowNum");
+                exportHeaders.add("文件名称|name");
+                exportHeaders.add("文件地址|fileName");
+                for (WorkKnowledgeBaseDynamicInfo field : rootDynamicFields) {
+                    exportHeaders.add(field.getFieldLabel() + "|dynamic_" + field.getFieldKey());
+                }
+                exportHeaders.add("内容属性|contentAttribute");
+                exportHeaders.add("创建人|createByName");
+                exportHeaders.add("创建时间|createTime");
+                exportHeaders.add("状态|statusLabel");
+                exportHeaders.add("是否人工赋分|isManualScoreLabel");
+                exportHeaders.add("所属子节点|childNodeName");
+                // 审核信息列(最多3位审核人,每人3列:姓名、时间、间隔)
+                for (int i = 1; i <= 3; i++) {
+                    exportHeaders.add("审核人" + i + "|auditor" + i + "Name");
+                    exportHeaders.add("审核时间" + i + "|auditor" + i + "Time");
+                    exportHeaders.add("审核间隔" + i + "(秒)|auditor" + i + "Gap");
+                }
+
+                // 查询该根节点下所有数据
+                WorkKnowledgeBaseShareInfo queryEntity = new WorkKnowledgeBaseShareInfo();
+                queryEntity.setRootId(rootId);
+                if (StringUtils.isNotBlank(name)) {
+                    queryEntity.setName(name);
+                }
+                queryEntity.setBeginDate(beginDate);
+                queryEntity.setEndDate(endDate);
+                String isManualScore = request.getParameter("isManualScore");
+                if (StringUtils.isNotBlank(isManualScore)) {
+                    queryEntity.setIsManualScore(isManualScore);
+                }
+                List<Map<String, Object>> dataList = shareService.findDynamicListAll(queryEntity, rootDynamicFields, dynamicConditions);
+                shareService.fillFirstAttachmentData(dataList);
+
+                // ===== 批量查询审核数据(LEFT JOIN 临时表获取 time_gap_seconds) =====
+                List<String> allFileIds = new ArrayList<>();
+                for (Map<String, Object> row : dataList) {
+                    Object idObj = row.get("id");
+                    if (idObj != null) {
+                        allFileIds.add(idObj.toString());
+                    }
+                }
+                // 按 file_id 分组:fileId -> List<Map>(已按 audit_time ASC 排序)
+                Map<String, List<Map<String, Object>>> auditDataMap = new HashMap<>();
+                if (!allFileIds.isEmpty()) {
+                    List<Map<String, Object>> allAuditData = auditRecordService.findAuditExportData(allFileIds);
+                    for (Map<String, Object> ad : allAuditData) {
+                        String fid = ad.get("fileId") != null ? ad.get("fileId").toString() : "";
+                        List<Map<String, Object>> list = auditDataMap.get(fid);
+                        if (list == null) {
+                            list = new ArrayList<>();
+                            auditDataMap.put(fid, list);
+                        }
+                        list.add(ad);
+                    }
+                }
+
+                // 处理导出数据
+                List<Map<String, Object>> exportData = new ArrayList<>();
+                int rowNum = 1;
+                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                for (Map<String, Object> row : dataList) {
+                    Map<String, Object> exportRow = new HashMap<>(row);
+                    exportRow.put("_rowNum", rowNum++);
+
+                    // 创建时间使用附件上传时间(work_attachment.create_date)替代原记录创建时间
+                    Object attachCreateDate = row.get("attachmentCreateDate");
+                    if (attachCreateDate instanceof Date) {
+                        exportRow.put("createTime", sdf.format((Date) attachCreateDate));
+                    } else if (attachCreateDate != null) {
+                        exportRow.put("createTime", attachCreateDate.toString());
+                    }
+
+                    // 状态映射
+                    String auditStatus = row.get("auditStatus") != null ? row.get("auditStatus").toString() : "";
+                    String statusLabel;
+                    switch (auditStatus) {
+                        case "0": statusLabel = "草稿"; break;
+                        case "1": statusLabel = "未审核"; break;
+                        case "2": statusLabel = "审核中"; break;
+                        case "4": statusLabel = "审核未通过"; break;
+                        case "5": statusLabel = "审核通过"; break;
+                        case "6": statusLabel = "待回答"; break;
+                        case "7": statusLabel = "回答中"; break;
+                        case "8": statusLabel = "已结束"; break;
+                        default: statusLabel = ""; break;
+                    }
+                    exportRow.put("statusLabel", statusLabel);
+                    // 是否人工赋分转换
+                    String manualScore = row.get("isManualScore") != null ? row.get("isManualScore").toString() : "";
+                    exportRow.put("isManualScoreLabel", "1".equals(manualScore) ? "是" : "否");
+                    // 内容属性转换
+                    String contentAttr = row.get("contentAttribute") != null ? row.get("contentAttribute").toString() : "";
+                    if ("0".equals(contentAttr)) {
+                        exportRow.put("contentAttribute", "原创");
+                    } else if ("1".equals(contentAttr)) {
+                        exportRow.put("contentAttribute", "转载");
+                    } else {
+                        exportRow.put("contentAttribute", "");
+                    }
+                    // 所属子节点名称
+                    String treeNodeId = row.get("treeNodeId") != null ? row.get("treeNodeId").toString() : "";
+                    String childNodeName = nodeNameMap.getOrDefault(treeNodeId, "");
+                    exportRow.put("childNodeName", childNodeName);
+
+                    // ===== 填充审核信息(最多3位审核人,数据来自临时表) =====
+                    String fileId = row.get("id") != null ? row.get("id").toString() : "";
+                    List<Map<String, Object>> auditDataList = auditDataMap.get(fileId);
+                    for (int i = 1; i <= 3; i++) {
+                        if (auditDataList != null && i <= auditDataList.size()) {
+                            Map<String, Object> ad = auditDataList.get(i - 1);
+                            // 审核人
+                            exportRow.put("auditor" + i + "Name", ad.get("auditorName") != null ? ad.get("auditorName").toString() : "");
+                            // 审核时间
+                            Object auditTimeObj = ad.get("auditTime");
+                            if (auditTimeObj instanceof Date) {
+                                exportRow.put("auditor" + i + "Time", sdf.format((Date) auditTimeObj));
+                            } else {
+                                exportRow.put("auditor" + i + "Time", auditTimeObj != null ? auditTimeObj.toString() : "");
+                            }
+                            // 审核间隔(秒,来自临时表 time_gap_seconds)
+                            Object gapObj = ad.get("timeGapSeconds");
+                            if (gapObj instanceof Number) {
+                                exportRow.put("auditor" + i + "Gap", ((Number) gapObj).longValue());
+                            } else {
+                                exportRow.put("auditor" + i + "Gap", "");
+                            }
+                        } else {
+                            exportRow.put("auditor" + i + "Name", "");
+                            exportRow.put("auditor" + i + "Time", "");
+                            exportRow.put("auditor" + i + "Gap", "");
+                        }
+                    }
+
+                    exportData.add(exportRow);
+                }
+
+                // Sheet名称处理
+                String sheetName = rootNode.getTreeName();
+                if (sheetName == null || sheetName.isEmpty()) {
+                    sheetName = "未命名分类";
+                }
+                sheetName = sheetName.replaceAll("[\\\\/:*?\\[\\]]", "_");
+                if (sheetName.length() > 31) {
+                    sheetName = sheetName.substring(0, 31);
+                }
+                String baseSheetName = sheetName;
+                int suffix = 2;
+                while (usedSheetNames.contains(sheetName)) {
+                    String suffixStr = "(" + suffix + ")";
+                    int maxBaseLen = 31 - suffixStr.length();
+                    sheetName = (baseSheetName.length() > maxBaseLen ? baseSheetName.substring(0, maxBaseLen) : baseSheetName) + suffixStr;
+                    suffix++;
+                }
+                usedSheetNames.add(sheetName);
+
+                excel.addSheet(sheetName, rootNode.getTreeName(), exportHeaders, exportData);
+                allExportData.addAll(exportData);
+            }
+
+            // ======================== 最后一个页签:汇总统计 ========================
+            int totalCount = allExportData.size();
+            int passedCount = 0, auditingCount = 0, pendingCount = 0, draftCount = 0, rejectedCount = 0;
+            for (Map<String, Object> row : allExportData) {
+                String status = row.get("auditStatus") != null ? row.get("auditStatus").toString() : "";
+                switch (status) {
+                    case "5": passedCount++; break;
+                    case "2": auditingCount++; break;
+                    case "1": pendingCount++; break;
+                    case "0": draftCount++; break;
+                    case "4": rejectedCount++; break;
+                }
+            }
+            List<String> summaryHeaders = new ArrayList<>();
+            summaryHeaders.add("统计项|label");
+            summaryHeaders.add("数量|count");
+            List<Map<String, Object>> summaryData = new ArrayList<>();
+            String[] labels = {"总数据量", "审核通过", "审核中", "未审核", "草稿", "审核未通过"};
+            int[] counts = {totalCount, passedCount, auditingCount, pendingCount, draftCount, rejectedCount};
+            for (int i = 0; i < labels.length; i++) {
+                Map<String, Object> row = new HashMap<>();
+                row.put("label", labels[i]);
+                row.put("count", counts[i]);
+                summaryData.add(row);
+            }
+            excel.addSheet("汇总统计", "汇总统计", summaryHeaders, summaryData);
+
+            String fileName = "知识库数据导出(含审核)_" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+            excel.write(response, fileName);
+            excel.dispose();
+
+        } catch (Exception e) {
+            throw new RuntimeException("导出失败:" + e.getMessage(), e);
+        }
+    }
+
+    // ======================== 月度汇总统计导出 ========================
+
+    /**
+     * 月度汇总统计导出(上传量、点赞量、阅读量)
+     */
+    @RequiresPermissions("workKnowledgeBase:share:export")
+    @RequestMapping(value = "monthlyStatsExport")
+    public void monthlyStatsExport(@RequestParam String beginMonth,
+                                    @RequestParam String endMonth,
+                                    HttpServletResponse response) {
+        try {
+            List<Map<String, Object>> statsData = shareService.getMonthlyStats(beginMonth, endMonth);
+
+            ExportKnowledgeBaseExcel excel = new ExportKnowledgeBaseExcel();
+            List<String> headers = new ArrayList<>();
+            headers.add("月份|month");
+            headers.add("上传数量|uploadCount");
+            headers.add("点赞数量|likeCount");
+            headers.add("阅读数量|readCount");
+
+            excel.addSheet("月度汇总", "月度汇总统计", headers, statsData);
+
+            String fileName = "月度汇总统计_" + beginMonth + "_" + endMonth + "_" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+            excel.write(response, fileName);
+            excel.dispose();
+        } catch (Exception e) {
+            throw new RuntimeException("导出失败:" + e.getMessage(), e);
+        }
+    }
+
     // ======================== 审核相关接口 ========================
 
     /**

+ 32 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseAuditRecordDao.xml

@@ -184,4 +184,36 @@
           AND del_flag = '0'
     </update>
 
+    <!-- ======================== 临时导出用(后期可删除) ======================== -->
+
+    <!-- 批量查询多个文件的审核记录(含审核人姓名,按file_id分组、audit_time正序) -->
+    <select id="findByFileIds" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseAuditRecord">
+        SELECT <include refid="auditRecordColumns"/>,
+               (SELECT su.name FROM sys_user su WHERE su.id = a.audit_user_id) AS "auditUserName"
+        FROM work_knowledge_base_audit_record a
+        WHERE a.del_flag = '0'
+          AND a.file_id IN
+        <foreach collection="fileIds" item="fid" open="(" separator="," close=")">
+            #{fid}
+        </foreach>
+        ORDER BY a.file_id, a.audit_time ASC
+    </select>
+
+    <!-- 查询导出用审核数据(LEFT JOIN 临时表获取 time_gap_seconds) -->
+    <select id="findAuditExportData" resultType="java.util.HashMap">
+        SELECT a.file_id     AS "fileId",
+               su.name        AS "auditorName",
+               a.audit_time   AS "auditTime",
+               t.time_gap_seconds AS "timeGapSeconds"
+        FROM work_knowledge_base_audit_record a
+        LEFT JOIN sys_user su ON su.id = a.audit_user_id
+        LEFT JOIN tmp_kb_detail_audit t ON t.audit_record_id = a.id
+        WHERE a.del_flag = '0'
+          AND a.file_id IN
+        <foreach collection="fileIds" item="fid" open="(" separator="," close=")">
+            #{fid}
+        </foreach>
+        ORDER BY a.file_id, a.audit_time ASC
+    </select>
+
 </mapper>

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

@@ -182,4 +182,17 @@
           AND status = 1
     </select>
 
+    <!-- 按月份统计点赞数量(有效点赞 status=1) -->
+    <select id="findMonthlyLikeStats" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(a.like_time, '%Y-%m') AS "month",
+               COUNT(*) AS "likeCount"
+        FROM work_knowledge_base_like_record a
+        WHERE a.del_flag = 0
+          AND a.status = 1
+          AND a.like_time &gt;= #{beginDate}
+          AND a.like_time &lt; #{endDate}
+        GROUP BY DATE_FORMAT(a.like_time, '%Y-%m')
+        ORDER BY "month" ASC
+    </select>
+
 </mapper>

+ 12 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseReadRecordDao.xml

@@ -179,4 +179,16 @@
         WHERE del_flag = 0
     </select>
 
+    <!-- 按月份统计阅读数量(去重用户数) -->
+    <select id="findMonthlyReadStats" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(a.first_read_time, '%Y-%m') AS "month",
+               COUNT(*) AS "readCount"
+        FROM work_knowledge_base_read_record a
+        WHERE a.del_flag = 0
+          AND a.first_read_time &gt;= #{beginDate}
+          AND a.first_read_time &lt; #{endDate}
+        GROUP BY DATE_FORMAT(a.first_read_time, '%Y-%m')
+        ORDER BY "month" ASC
+    </select>
+
 </mapper>

+ 15 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseShareInfoDao.xml

@@ -522,4 +522,19 @@
           AND del_flag = '0'
     </select>
 
+    <!-- 按月份统计上传数量(仅统计有对应文件上传的记录) -->
+    <select id="findMonthlyUploadStats" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(s.create_date, '%Y-%m') AS "month",
+               COUNT(DISTINCT s.id) AS "uploadCount"
+        FROM work_knowledge_base_share_info s
+        INNER JOIN work_attachment att ON att.attachment_id = s.id
+            AND att.attachment_flag = '199'
+            AND att.del_flag = '0'
+        WHERE s.del_flag = '0'
+          AND s.create_date &gt;= #{beginDate}
+          AND s.create_date &lt; #{endDate}
+        GROUP BY DATE_FORMAT(s.create_date, '%Y-%m')
+        ORDER BY "month" ASC
+    </select>
+
 </mapper>

+ 74 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareList.jsp

@@ -867,6 +867,74 @@
             }, 60000);
         }
 
+        /** 月度汇总导出弹窗 */
+        function openMonthlyStatsDialog() {
+            // 默认最近三个月
+            var now = new Date();
+            var endMonth = formatMonth(now);
+            var startDate = new Date(now.getFullYear(), now.getMonth() - 2, 1);
+            var beginMonth = formatMonth(startDate);
+
+            var html = '<div style="padding:20px;">'
+                + '<div class="layui-form-item">'
+                + '<label class="layui-form-label">开始月份</label>'
+                + '<div class="layui-input-inline"><input type="month" id="msBeginMonth" class="layui-input" value="' + beginMonth + '"></div>'
+                + '</div>'
+                + '<div class="layui-form-item">'
+                + '<label class="layui-form-label">结束月份</label>'
+                + '<div class="layui-input-inline"><input type="month" id="msEndMonth" class="layui-input" value="' + endMonth + '"></div>'
+                + '</div>'
+                + '</div>';
+
+            top.layer.open({
+                type: 1,
+                area: ['400px', '260px'],
+                title: '月度汇总统计导出',
+                content: html,
+                btn: ['导出', '取消'],
+                btn1: function(index, layero) {
+                    var bm = layero.find('#msBeginMonth').val();
+                    var em = layero.find('#msEndMonth').val();
+                    if (!bm || !em) {
+                        top.layer.msg('请选择月份区间', {icon: 5});
+                        return;
+                    }
+                    if (bm > em) {
+                        top.layer.msg('开始月份不能晚于结束月份', {icon: 5});
+                        return;
+                    }
+                    // 通过iframe下载
+                    var iframeName = 'monthlyStatsIframe_' + new Date().getTime();
+                    var iframe = $('<iframe>', {
+                        name: iframeName,
+                        style: 'display:none;',
+                        id: iframeName
+                    }).appendTo('body');
+                    var form = $('<form>', {
+                        method: 'GET',
+                        action: '${ctx}/workKnowledgeBase/share/monthlyStatsExport',
+                        target: iframeName,
+                        style: 'display:none;'
+                    }).appendTo('body');
+                    $('<input>').attr({type:'hidden', name:'beginMonth', value:bm}).appendTo(form);
+                    $('<input>').attr({type:'hidden', name:'endMonth', value:em}).appendTo(form);
+                    form.appendTo('body').submit();
+                    setTimeout(function() {
+                        form.remove();
+                        iframe.remove();
+                    }, 30000);
+                    top.layer.close(index);
+                },
+                btn2: function(index) { top.layer.close(index); }
+            });
+        }
+
+        function formatMonth(d) {
+            var y = d.getFullYear();
+            var m = ('0' + (d.getMonth() + 1)).slice(-2);
+            return y + '-' + m;
+        }
+
         /** 获取审核状态显示配置 */
         /*function getWorkKnowledgeAuditState(auditStatus) {
             var statusMap = {0:'草稿', 1:'未审核', 2:'审核中', 4:'审核未通过', 5:'审核通过'};
@@ -1131,6 +1199,12 @@
                                      导出积分统计
                                 </button>
                             </shiro:hasPermission>
+                            <shiro:hasPermission name="workKnowledgeBase:share:export">
+                                <button class="layui-btn layui-btn-sm layui-bg-brown"
+                                        onclick="openMonthlyStatsDialog()" style="background-color:#9c27b0;">
+                                     月度汇总
+                                </button>
+                            </shiro:hasPermission>
                             <shiro:hasPermission name="workKnowledgeBase:share:import">
                                 <button class="layui-btn layui-btn-sm layui-bg-cyan"
                                         onclick="openImportFileBatchDialog()">