Bladeren bron

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

徐滕 2 weken geleden
bovenliggende
commit
c0e311aee6
25 gewijzigde bestanden met toevoegingen van 1488 en 29 verwijderingen
  1. 316 0
      src/main/java/com/jeeplus/common/utils/ImageUtil.java
  2. 1 2
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseClickRecordDao.java
  3. 6 2
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseLikeRecordDao.java
  4. 49 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBasePointDetailDao.java
  5. 1 1
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseReadRecordDao.java
  6. 22 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBasePoint.java
  7. 58 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBasePointDetailService.java
  8. 12 2
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseReadLikeService.java
  9. 6 1
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java
  10. 188 1
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBasePointController.java
  11. 5 1
      src/main/java/com/jeeplus/modules/ruralprojectrecords/service/RuralProjectMessageElectronicSealService.java
  12. 5 1
      src/main/java/com/jeeplus/modules/ruralprojectrecords/service/changZhouSignature/RuralProjectMessageElectronicSealCZService.java
  13. 5 1
      src/main/java/com/jeeplus/modules/ruralprojectrecords/service/yanChengSignature/RuralProjectMessageElectronicSealYCService.java
  14. 5 1
      src/main/java/com/jeeplus/modules/workfullmanage/service/WorkFullManageService.java
  15. 1 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseClickRecordDao.xml
  16. 9 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseLikeRecordDao.xml
  17. 217 5
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml
  18. 1 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseReadRecordDao.xml
  19. 4 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseClickDetail.jsp
  20. 4 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseLikeDetail.jsp
  21. 244 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointExchangeHistory.jsp
  22. 36 2
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointList.jsp
  23. 258 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointRanking.jsp
  24. 4 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseReadDetail.jsp
  25. 31 9
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareList.jsp

+ 316 - 0
src/main/java/com/jeeplus/common/utils/ImageUtil.java

@@ -3,7 +3,9 @@ package com.jeeplus.common.utils;
 import sun.misc.BASE64Encoder;
 
 import javax.imageio.ImageIO;
+import java.awt.*;
 import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -61,4 +63,318 @@ public class ImageUtil {
 
         return imageStr;
     }
+
+    // ==================== 签字章图片预处理 ====================
+
+    /**
+     * 签字章图片预处理:仅去阴影、去背景、裁剪白边,保持原始尺寸
+     * 保留灰度图的平滑过渡(抗锯齿),不做二值化和缩放
+     * @param srcPath 原始图片路径
+     * @return 处理后的base64字符串,以及宽高信息(单位pt)
+     */
+    public static SignImageResult processSignImage(String srcPath) {
+        BufferedImage original = readImage(srcPath);
+        if (original == null) {
+            return null;
+        }
+        // 1. 转灰度
+        BufferedImage gray = toGrayscale(original);
+        // 2. 去阴影(背景估算 + 除法归一化)
+        BufferedImage normalized = removeShadow(gray);
+        // 3. 裁剪白边(灰度图直接裁剪,保留平滑过渡)
+        BufferedImage cropped = trimWhiteSpace(normalized);
+        // 4. 保持原始尺寸,不做缩放
+        int width = cropped.getWidth();
+        int height = cropped.getHeight();
+        // 5. 转为base64
+        String base64 = bufferedImageToBase64(cropped, "png");
+        // 6. 计算pt尺寸(1px ≈ 0.75pt)
+        float widthPt = width * 0.75f;
+        float heightPt = height * 0.75f;
+        return new SignImageResult(base64, widthPt, heightPt);
+    }
+
+    /**
+     * 转灰度图(处理透明背景:透明像素设为白色)
+     */
+    private static BufferedImage toGrayscale(BufferedImage src) {
+        int w = src.getWidth();
+        int h = src.getHeight();
+        BufferedImage gray = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
+        // 判断是否有透明通道
+        boolean hasAlpha = src.getColorModel().hasAlpha();
+        if (hasAlpha) {
+            // 逐像素处理:透明像素→白色,非透明→灰度
+            for (int y = 0; y < h; y++) {
+                for (int x = 0; x < w; x++) {
+                    int argb = src.getRGB(x, y);
+                    int alpha = (argb >> 24) & 0xFF;
+                    if (alpha < 30) {
+                        // 透明或近透明 → 白色
+                        gray.getRaster().setSample(x, y, 0, 255);
+                    } else {
+                        // 非透明 → 取灰度值
+                        int r = (argb >> 16) & 0xFF;
+                        int g = (argb >> 8) & 0xFF;
+                        int b = argb & 0xFF;
+                        int grayVal = (int) (0.299 * r + 0.587 * g + 0.114 * b);
+                        gray.getRaster().setSample(x, y, 0, grayVal);
+                    }
+                }
+            }
+        } else {
+            // 无透明通道,直接绘制
+            Graphics2D g = gray.createGraphics();
+            g.drawImage(src, 0, 0, null);
+            g.dispose();
+        }
+        return gray;
+    }
+
+    /**
+     * 去阴影核心算法:背景估算 + 除法归一化
+     * 原理:用大核高斯模糊估算背景光照,然后用原图除以背景来消除不均匀光照(阴影)
+     */
+    private static BufferedImage removeShadow(BufferedImage gray) {
+        int w = gray.getWidth();
+        int h = gray.getHeight();
+        // 高斯核半径,取图像较长边的1/10,保证能覆盖签字笔画宽度
+        int radius = Math.max(15, Math.max(w, h) / 10);
+        // 用大核模糊来估算背景
+        float[] background = gaussianBlur(gray, radius);
+        BufferedImage result = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
+        for (int y = 0; y < h; y++) {
+            for (int x = 0; x < w; x++) {
+                int pixel = gray.getRaster().getSample(x, y, 0);
+                float bg = background[y * w + x];
+                // 除法归一化:原图像素 / 背景像素 * 255
+                // 如果背景很暗(阴影区),除法后该区域会变亮,阴影被消除
+                int normalized;
+                if (bg < 1) {
+                    normalized = 255;
+                } else {
+                    normalized = Math.min(255, (int) (pixel * 255.0f / bg));
+                }
+                result.getRaster().setSample(x, y, 0, normalized);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 大核高斯模糊(用于估算背景光照)
+     * 使用行列分离的一维卷积优化,避免二维卷积的高计算量
+     */
+    private static float[] gaussianBlur(BufferedImage img, int radius) {
+        int w = img.getWidth();
+        int h = img.getHeight();
+        float[] pixels = new float[w * h];
+        for (int y = 0; y < h; y++) {
+            for (int x = 0; x < w; x++) {
+                pixels[y * w + x] = img.getRaster().getSample(x, y, 0);
+            }
+        }
+        // 生成一维高斯核
+        int kernelSize = radius * 2 + 1;
+        float[] kernel = new float[kernelSize];
+        float sigma = radius / 3.0f;
+        float sum = 0;
+        for (int i = 0; i < kernelSize; i++) {
+            float x = i - radius;
+            kernel[i] = (float) Math.exp(-(x * x) / (2 * sigma * sigma));
+            sum += kernel[i];
+        }
+        for (int i = 0; i < kernelSize; i++) {
+            kernel[i] /= sum;
+        }
+        // 水平方向卷积
+        float[] temp = new float[w * h];
+        for (int y = 0; y < h; y++) {
+            for (int x = 0; x < w; x++) {
+                float val = 0;
+                for (int k = -radius; k <= radius; k++) {
+                    int sx = Math.min(w - 1, Math.max(0, x + k));
+                    val += pixels[y * w + sx] * kernel[k + radius];
+                }
+                temp[y * w + x] = val;
+            }
+        }
+        // 垂直方向卷积
+        float[] result = new float[w * h];
+        for (int y = 0; y < h; y++) {
+            for (int x = 0; x < w; x++) {
+                float val = 0;
+                for (int k = -radius; k <= radius; k++) {
+                    int sy = Math.min(h - 1, Math.max(0, y + k));
+                    val += temp[sy * w + x] * kernel[k + radius];
+                }
+                result[y * w + x] = val;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 自适应阈值二值化(Sauvola算法改进版)
+     * 对每个像素根据其邻域的均值和标准差动态计算阈值
+     * 能有效处理光照不均的情况,比全局阈值效果好得多
+     */
+    private static BufferedImage adaptiveBinarize(BufferedImage gray) {
+        int w = gray.getWidth();
+        int h = gray.getHeight();
+        int blockSize = 35; // 邻域大小,必须为奇数
+        int halfBlock = blockSize / 2;
+        double k = 0.2; // Sauvola参数,控制阈值对标准差的敏感度
+        int R = 128; // 标准差归一化常数
+
+        BufferedImage result = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
+
+        // 计算积分图和积分平方图,加速邻域统计计算
+        long[] sumTable = new long[(w + 1) * (h + 1)];
+        long[] sqTable = new long[(w + 1) * (h + 1)];
+        int stride = w + 1;
+
+        for (int y = 0; y < h; y++) {
+            long rowSum = 0;
+            long rowSqSum = 0;
+            for (int x = 0; x < w; x++) {
+                int pixel = gray.getRaster().getSample(x, y, 0);
+                rowSum += pixel;
+                rowSqSum += (long) pixel * pixel;
+                sumTable[(y + 1) * stride + (x + 1)] = rowSum + sumTable[y * stride + (x + 1)];
+                sqTable[(y + 1) * stride + (x + 1)] = rowSqSum + sqTable[y * stride + (x + 1)];
+            }
+        }
+
+        for (int y = 0; y < h; y++) {
+            for (int x = 0; x < w; x++) {
+                int x1 = Math.max(0, x - halfBlock);
+                int y1 = Math.max(0, y - halfBlock);
+                int x2 = Math.min(w - 1, x + halfBlock);
+                int y2 = Math.min(h - 1, y + halfBlock);
+                int count = (x2 - x1 + 1) * (y2 - y1 + 1);
+
+                long s = sumTable[(y2 + 1) * stride + (x2 + 1)]
+                        - sumTable[y1 * stride + (x2 + 1)]
+                        - sumTable[(y2 + 1) * stride + x1]
+                        + sumTable[y1 * stride + x1];
+                long sq = sqTable[(y2 + 1) * stride + (x2 + 1)]
+                        - sqTable[y1 * stride + (x2 + 1)]
+                        - sqTable[(y2 + 1) * stride + x1]
+                        + sqTable[y1 * stride + x1];
+
+                double mean = (double) s / count;
+                double variance = (double) sq / count - mean * mean;
+                double stddev = Math.sqrt(Math.max(0, variance));
+
+                // Sauvola阈值公式
+                double threshold = mean * (1 + k * (stddev / R - 1));
+
+                int pixel = gray.getRaster().getSample(x, y, 0);
+                // 签字/印章颜色深(像素值小),背景颜色浅(像素值大)
+                // 像素值 < 阈值 → 前景(签字),设为0(黑色)
+                // 像素值 >= 阈值 → 背景,设为255(白色)
+                result.getRaster().setSample(x, y, 0, pixel < threshold ? 0 : 255);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 裁剪白边:检测图像中非白色区域的最小包围矩形并裁剪
+     */
+    private static BufferedImage trimWhiteSpace(BufferedImage img) {
+        int w = img.getWidth();
+        int h = img.getHeight();
+        int minX = w, minY = h, maxX = 0, maxY = 0;
+        boolean found = false;
+
+        for (int y = 0; y < h; y++) {
+            for (int x = 0; x < w; x++) {
+                int pixel = img.getRaster().getSample(x, y, 0);
+                if (pixel < 250) { // 非纯白区域(留一点容差)
+                    found = true;
+                    if (x < minX) minX = x;
+                    if (x > maxX) maxX = x;
+                    if (y < minY) minY = y;
+                    if (y > maxY) maxY = y;
+                }
+            }
+        }
+        if (!found) {
+            return img; // 全白图,不裁剪
+        }
+        // 四周留2px边距,避免签字贴边
+        minX = Math.max(0, minX - 2);
+        minY = Math.max(0, minY - 2);
+        maxX = Math.min(w - 1, maxX + 2);
+        maxY = Math.min(h - 1, maxY + 2);
+        return img.getSubimage(minX, minY, maxX - minX + 1, maxY - minY + 1);
+    }
+
+    /**
+     * 等比缩放图片
+     */
+    private static BufferedImage scaleImage(BufferedImage src, int targetWidth, int targetHeight) {
+        BufferedImage result = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_BYTE_GRAY);
+        Graphics2D g = result.createGraphics();
+        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+        g.drawImage(src, 0, 0, targetWidth, targetHeight, null);
+        g.dispose();
+        return result;
+    }
+
+    /**
+     * BufferedImage转base64字符串
+     */
+    private static String bufferedImageToBase64(BufferedImage image, String format) {
+        try {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            ImageIO.write(image, format, baos);
+            byte[] data = baos.toByteArray();
+            BASE64Encoder encoder = new BASE64Encoder();
+            return encoder.encode(data);
+        } catch (IOException e) {
+            e.printStackTrace();
+            return "";
+        }
+    }
+
+    /**
+     * 读取图片文件
+     */
+    private static BufferedImage readImage(String path) {
+        if (path == null || path.trim().isEmpty()) {
+            return null;
+        }
+        File file = new File(path);
+        if (!file.exists() || file.isDirectory()) {
+            return null;
+        }
+        try {
+            return ImageIO.read(file);
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 签字章图片预处理结果
+     */
+    public static class SignImageResult {
+        private String base64;     // 处理后的base64字符串
+        private float widthPt;     // 推荐宽度(pt)
+        private float heightPt;    // 推荐高度(pt)
+
+        public SignImageResult(String base64, float widthPt, float heightPt) {
+            this.base64 = base64;
+            this.widthPt = widthPt;
+            this.heightPt = heightPt;
+        }
+
+        public String getBase64() { return base64; }
+        public float getWidthPt() { return widthPt; }
+        public float getHeightPt() { return heightPt; }
+    }
 }

+ 1 - 2
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseClickRecordDao.java

@@ -1,7 +1,6 @@
 package com.jeeplus.modules.WorkKnowledgeBase.dao;
 
 import com.jeeplus.common.persistence.CrudDao;
-import com.jeeplus.common.persistence.Page;
 import com.jeeplus.common.persistence.annotation.MyBatisDao;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseClickRecord;
 import org.apache.ibatis.annotations.Param;
@@ -39,5 +38,5 @@ public interface WorkKnowledgeBaseClickRecordDao extends CrudDao<WorkKnowledgeBa
     /**
      * 查询文件的点击详情列表(分页)
      */
-    List<WorkKnowledgeBaseClickRecord> findClickDetailPage(Page<WorkKnowledgeBaseClickRecord> page, @Param("fileId") String fileId);
+    List<WorkKnowledgeBaseClickRecord> findClickDetailPage(@Param("fileId") String fileId, @Param("offset") int offset, @Param("limit") int limit);
 }

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

@@ -1,7 +1,6 @@
 package com.jeeplus.modules.WorkKnowledgeBase.dao;
 
 import com.jeeplus.common.persistence.CrudDao;
-import com.jeeplus.common.persistence.Page;
 import com.jeeplus.common.persistence.annotation.MyBatisDao;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseLikeRecord;
 import org.apache.ibatis.annotations.Param;
@@ -31,6 +30,11 @@ public interface WorkKnowledgeBaseLikeRecordDao extends CrudDao<WorkKnowledgeBas
     Integer countLikesByFileId(@Param("fileId") String fileId);
 
     /**
+     * 统计文件的点赞记录总数(含已取消,用于详情页分页)
+     */
+    Integer countAllLikesByFileId(@Param("fileId") String fileId);
+
+    /**
      * 查询文件的点赞详情列表(不分页)
      */
     List<WorkKnowledgeBaseLikeRecord> findLikeDetailList(@Param("fileId") String fileId);
@@ -38,7 +42,7 @@ public interface WorkKnowledgeBaseLikeRecordDao extends CrudDao<WorkKnowledgeBas
     /**
      * 查询文件的点赞详情列表(分页)
      */
-    List<WorkKnowledgeBaseLikeRecord> findLikeDetailPage(Page<WorkKnowledgeBaseLikeRecord> page, @Param("fileId") String fileId);
+    List<WorkKnowledgeBaseLikeRecord> findLikeDetailPage(@Param("fileId") String fileId, @Param("offset") int offset, @Param("limit") int limit);
 
     /**
      * 查询所有已被点赞过(status=1)的文档 ID(去重)

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

@@ -224,4 +224,53 @@ public interface WorkKnowledgeBasePointDetailDao extends CrudDao<WorkKnowledgeBa
      * @return 记录数
      */
     int countUserBasicExchangeRecords(@Param("userId") String userId);
+
+    /**
+     * 积分排行榜查询(同时返回基础积分和贡献积分的获取、兑换、剩余)
+     * 按总积分(基础获取 + 贡献获取)倒序排列
+     * @return 排行榜数据列表
+     */
+    List<WorkKnowledgeBasePoint> findUserPointRankingList();
+
+    /**
+     * 积分排行榜分页查询(支持人员/部门/日期筛选)
+     * @param entity 查询参数(submitterId/officeId/gainBeginDate/gainEndDate)
+     * @return 排行榜分页数据
+     */
+    List<WorkKnowledgeBasePoint> findUserPointRankingPage(WorkKnowledgeBasePoint entity);
+
+    /**
+     * 积分排行榜分页总数
+     * @param entity 查询参数
+     * @return 符合条件的记录数
+     */
+    int findUserPointRankingCount(WorkKnowledgeBasePoint entity);
+
+    /**
+     * 兑换历史记录查询(按用户汇总,不按月份拆分)
+     * 基础积分兑换 category_id='1',贡献积分兑换 category_id='2'
+     * @param exchangeMonth 兑换月份筛选(可选,格式 2026-10)
+     * @return 兑换历史列表,按总兑换积分倒序
+     */
+    List<WorkKnowledgeBasePoint> findExchangeHistoryList(@Param("exchangeMonth") String exchangeMonth);
+
+    /**
+     * 兑换历史分页查询(支持人员/部门/月份筛选)
+     * @param entity 查询参数(submitterId/officeId/exchangeMonth)
+     * @return 兑换历史分页数据
+     */
+    List<WorkKnowledgeBasePoint> findExchangeHistoryPage(WorkKnowledgeBasePoint entity);
+
+    /**
+     * 兑换历史分页总数
+     * @param entity 查询参数
+     * @return 符合条件的记录数
+     */
+    int findExchangeHistoryCount(WorkKnowledgeBasePoint entity);
+
+    /**
+     * 获取所有有兑换记录的月份列表(去重,倒序)
+     * @return 月份列表,如 ["2026-10", "2026-09", ...]
+     */
+    List<String> findExchangeMonths();
 }

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

@@ -44,7 +44,7 @@ public interface WorkKnowledgeBaseReadRecordDao extends CrudDao<WorkKnowledgeBas
     /**
      * 查询文件的阅读详情列表(分页)
      */
-    List<WorkKnowledgeBaseReadRecord> findReadDetailPage(Page<WorkKnowledgeBaseReadRecord> page, @Param("fileId") String fileId);
+    List<WorkKnowledgeBaseReadRecord> findReadDetailPage(@Param("fileId") String fileId, @Param("offset") int offset, @Param("limit") int limit);
 
     /**
      * 查询所有已被阅读过的文档 ID(去重)

+ 22 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBasePoint.java

@@ -97,6 +97,12 @@ public class WorkKnowledgeBasePoint extends DataEntity<WorkKnowledgeBasePoint> {
 	/** 兑换积分额度(绝对值) */
 	private Integer exchangePoint;
 
+	/** 兑换月份(如 2026-05,用于兑换历史列表) */
+	private String exchangeMonth;
+
+	/** 兑换类型(basic-基础积分,contribution-贡献积分,用于兑换历史列表) */
+	private String exchangeType;
+
 	/** 是否手动赋分筛选(0否 1是,仅用于查询条件) */
 	private String isManualScore;
 	
@@ -419,4 +425,20 @@ public class WorkKnowledgeBasePoint extends DataEntity<WorkKnowledgeBasePoint> {
 	public void setSortRule(String sortRule) {
 		this.sortRule = sortRule;
 	}
+
+	public String getExchangeMonth() {
+		return exchangeMonth;
+	}
+
+	public void setExchangeMonth(String exchangeMonth) {
+		this.exchangeMonth = exchangeMonth;
+	}
+
+	public String getExchangeType() {
+		return exchangeType;
+	}
+
+	public void setExchangeType(String exchangeType) {
+		this.exchangeType = exchangeType;
+	}
 }

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

@@ -217,4 +217,62 @@ public class WorkKnowledgeBasePointDetailService extends CrudService<WorkKnowled
         page.setList(list);
         return page;
     }
+
+    /**
+     * 积分排行榜查询(同时返回基础积分和贡献积分的获取、兑换、剩余)
+     * 按总积分(基础获取 + 贡献获取)倒序排列
+     * @return 排行榜数据列表
+     */
+    public List<WorkKnowledgeBasePoint> findUserPointRankingList() {
+        return pointDetailDao.findUserPointRankingList();
+    }
+
+    /**
+     * 积分排行榜分页查询(支持人员/部门/日期筛选)
+     * @param page 分页对象
+     * @param entity 查询参数
+     * @return 分页结果
+     */
+    public Page<WorkKnowledgeBasePoint> findUserPointRankingPage(Page<WorkKnowledgeBasePoint> page, WorkKnowledgeBasePoint entity) {
+        int count = pointDetailDao.findUserPointRankingCount(entity);
+        page.setCount(count);
+        page.setCountFlag(false);
+        entity.setPage(page);
+        List<WorkKnowledgeBasePoint> list = pointDetailDao.findUserPointRankingPage(entity);
+        page.setList(list);
+        return page;
+    }
+
+    /**
+     * 兑换历史记录查询(按用户汇总,不按月份拆分)
+     * @param exchangeMonth 兑换月份筛选(可选,格式 2026-10)
+     * @return 兑换历史列表,按总兑换积分倒序
+     */
+    public List<WorkKnowledgeBasePoint> findExchangeHistoryList(String exchangeMonth) {
+        return pointDetailDao.findExchangeHistoryList(exchangeMonth);
+    }
+
+    /**
+     * 兑换历史分页查询(支持人员/部门/月份筛选)
+     * @param page 分页对象
+     * @param entity 查询参数
+     * @return 分页结果
+     */
+    public Page<WorkKnowledgeBasePoint> findExchangeHistoryPage(Page<WorkKnowledgeBasePoint> page, WorkKnowledgeBasePoint entity) {
+        int count = pointDetailDao.findExchangeHistoryCount(entity);
+        page.setCount(count);
+        page.setCountFlag(false);
+        entity.setPage(page);
+        List<WorkKnowledgeBasePoint> list = pointDetailDao.findExchangeHistoryPage(entity);
+        page.setList(list);
+        return page;
+    }
+
+    /**
+     * 获取所有有兑换记录的月份列表(去重,倒序)
+     * @return 月份列表,如 ["2026-10", "2026-09", ...]
+     */
+    public List<String> findExchangeMonths() {
+        return pointDetailDao.findExchangeMonths();
+    }
 }

+ 12 - 2
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseReadLikeService.java

@@ -238,7 +238,12 @@ public class WorkKnowledgeBaseReadLikeService extends CrudService<WorkKnowledgeB
      * 查询文件的阅读详情列表(分页)
      */
     public Page<WorkKnowledgeBaseReadRecord> findReadDetailPage(Page<WorkKnowledgeBaseReadRecord> page, String fileId) {
-        page.setList(readRecordDao.findReadDetailPage(page, fileId));
+        int count = readRecordDao.countReadersByFileId(fileId);
+        page.setCount(count);
+        page.setCountFlag(false);
+        page.initialize();
+        int offset = (page.getPageNo() - 1) * page.getPageSize();
+        page.setList(readRecordDao.findReadDetailPage(fileId, offset, page.getPageSize()));
         return page;
     }
 
@@ -253,7 +258,12 @@ public class WorkKnowledgeBaseReadLikeService extends CrudService<WorkKnowledgeB
      * 查询文件的点赞详情列表(分页)
      */
     public Page<WorkKnowledgeBaseLikeRecord> findLikeDetailPage(Page<WorkKnowledgeBaseLikeRecord> page, String fileId) {
-        page.setList(likeRecordDao.findLikeDetailPage(page, fileId));
+        int count = likeRecordDao.countAllLikesByFileId(fileId);
+        page.setCount(count);
+        page.setCountFlag(false);
+        page.initialize();
+        int offset = (page.getPageNo() - 1) * page.getPageSize();
+        page.setList(likeRecordDao.findLikeDetailPage(fileId, offset, page.getPageSize()));
         return page;
     }
 

+ 6 - 1
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java

@@ -2829,7 +2829,12 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
      * 查询文件的点击详情列表(分页)
      */
     public Page<WorkKnowledgeBaseClickRecord> findClickDetailPage(Page<WorkKnowledgeBaseClickRecord> page, String fileId) {
-        page.setList(clickRecordDao.findClickDetailPage(page, fileId));
+        int count = clickRecordDao.countClickersByFileId(fileId);
+        page.setCount(count);
+        page.setCountFlag(false);
+        page.initialize();
+        int offset = (page.getPageNo() - 1) * page.getPageSize();
+        page.setList(clickRecordDao.findClickDetailPage(fileId, offset, page.getPageSize()));
         return page;
     }
 

+ 188 - 1
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBasePointController.java

@@ -466,7 +466,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
                         // 扣分标准的积分单独汇总
                         deductionByUser.merge(uid, pts, Integer::sum);
                     } else {
-                        pointsByRule.computeIfAbsent(uid, k -> new java.util.HashMap<>()).put(rid, pts);
+                        pointsByRule.computeIfAbsent(uid, k -> new java.util.HashMap<>()).merge(rid, pts, Integer::sum);
                     }
                 }
             }
@@ -955,4 +955,191 @@ public class WorkKnowledgeBasePointController extends BaseController {
         return result;
     }
 
+    /**
+     * 积分排行榜页面(通用列表格式:查询表单 + Layui表格 + 分页)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:listAll")
+    @RequestMapping(value = "rankingPage")
+    public String rankingPage(WorkKnowledgeBasePoint workKnowledgeBasePoint, HttpServletRequest request, HttpServletResponse response, Model model) {
+        // 设置默认值为 "0"(全部积分)
+        if (StringUtils.isBlank(workKnowledgeBasePoint.getColumnId())) {
+            workKnowledgeBasePoint.setColumnId("0");
+        }
+        System.out.println("========== rankingPage columnId = " + workKnowledgeBasePoint.getColumnId());
+        Page<WorkKnowledgeBasePoint> page = workKnowledgeBasePointDetailService.findUserPointRankingPage(new Page<>(request, response), workKnowledgeBasePoint);
+        model.addAttribute("page", page);
+        model.addAttribute("workKnowledgeBasePoint", workKnowledgeBasePoint);
+        return "modules/WorkKnowledgeBase/workKnowledgeBasePointRanking";
+    }
+
+    /**
+     * 兑换历史记录页面(通用列表格式:查询表单 + Layui表格 + 分页)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:listAll")
+    @RequestMapping(value = "exchangeHistoryPage")
+    public String exchangeHistoryPage(WorkKnowledgeBasePoint workKnowledgeBasePoint, HttpServletRequest request, HttpServletResponse response, Model model) {
+        Page<WorkKnowledgeBasePoint> page = workKnowledgeBasePointDetailService.findExchangeHistoryPage(new Page<>(request, response), workKnowledgeBasePoint);
+        List<String> exchangeMonths = workKnowledgeBasePointDetailService.findExchangeMonths();
+        model.addAttribute("page", page);
+        model.addAttribute("workKnowledgeBasePoint", workKnowledgeBasePoint);
+        model.addAttribute("exchangeMonths", exchangeMonths);
+        return "modules/WorkKnowledgeBase/workKnowledgeBasePointExchangeHistory";
+    }
+
+    /**
+     * 导出兑换历史记录
+     * @param exType all-全部, contribution-贡献, basic-基础
+     */
+    @RequiresPermissions("workKnowledgeBase:point:listAll")
+    @RequestMapping(value = "exportExchangeHistory")
+    public void exportExchangeHistory(@RequestParam(required = false, defaultValue = "all") String exType,
+                                      WorkKnowledgeBasePoint workKnowledgeBasePoint,
+                                      HttpServletResponse response, RedirectAttributes redirectAttributes) {
+        try {
+            // 使用分页查询方法获取全部数据(不分页,导出全部)
+            Page<WorkKnowledgeBasePoint> page = new Page<>();
+            page.setPageNo(1);
+            page.setPageSize(99999);
+            List<WorkKnowledgeBasePoint> exchangeList = workKnowledgeBasePointDetailService.findExchangeHistoryPage(page, workKnowledgeBasePoint).getList();
+
+            String fileName;
+            String[] headers;
+
+            switch (exType) {
+                case "contribution":
+                    fileName = "贡献积分兑换记录" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+                    headers = new String[]{"排名", "姓名", "所属部门", "手机号", "贡献积分兑换"};
+                    break;
+                case "basic":
+                    fileName = "基础积分兑换记录" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+                    headers = new String[]{"排名", "姓名", "所属部门", "手机号", "基础积分兑换"};
+                    break;
+                default:
+                    fileName = "积分兑换记录" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+                    headers = new String[]{"排名", "姓名", "所属部门", "手机号",
+                            "基础积分兑换", "贡献积分兑换", "总兑换积分"};
+                    break;
+            }
+
+            ExportExcel exportExcel = new ExportExcel(fileName, headers);
+
+            int rank = 1;
+            for (WorkKnowledgeBasePoint item : exchangeList) {
+                int basicExchanged = item.getExchangedBasicPoint() != null ? item.getExchangedBasicPoint() : 0;
+                int contribExchanged = item.getExchangedContributionPoint() != null ? item.getExchangedContributionPoint() : 0;
+                int totalExchanged = item.getExchangePoint() != null ? item.getExchangePoint() : 0;
+
+                org.apache.poi.ss.usermodel.Row row = exportExcel.addRow();
+                int col = 0;
+                exportExcel.addCell(row, col++, rank++, 2, Integer.class);
+                exportExcel.addCell(row, col++, item.getUserName(), 2, String.class);
+                exportExcel.addCell(row, col++, item.getOfficeName(), 2, String.class);
+                exportExcel.addCell(row, col++, item.getMobile() != null ? item.getMobile() : (item.getPhone() != null ? item.getPhone() : ""), 2, String.class);
+
+                switch (exType) {
+                    case "contribution":
+                        exportExcel.addCell(row, col++, contribExchanged, 2, Integer.class);
+                        break;
+                    case "basic":
+                        exportExcel.addCell(row, col++, basicExchanged, 2, Integer.class);
+                        break;
+                    default:
+                        exportExcel.addCell(row, col++, basicExchanged, 2, Integer.class);
+                        exportExcel.addCell(row, col++, contribExchanged, 2, Integer.class);
+                        exportExcel.addCell(row, col++, totalExchanged, 2, Integer.class);
+                        break;
+                }
+            }
+
+            exportExcel.write(response, fileName).dispose();
+        } catch (Exception e) {
+            addMessage(redirectAttributes, "导出兑换记录失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 导出积分排行榜
+     * @param rankingType all-全部, contribution-贡献, basic-基础
+     */
+    @RequiresPermissions("workKnowledgeBase:point:listAll")
+    @RequestMapping(value = "exportRanking")
+    public void exportRanking(@RequestParam(required = false, defaultValue = "0") String rankingType,
+                              WorkKnowledgeBasePoint workKnowledgeBasePoint,
+                              HttpServletResponse response, RedirectAttributes redirectAttributes) {
+        try {
+            // 使用分页查询方法获取全部数据(不分页,导出全部)
+            Page<WorkKnowledgeBasePoint> page = new Page<>();
+            page.setPageNo(1);
+            page.setPageSize(99999);
+            List<WorkKnowledgeBasePoint> rankingList = workKnowledgeBasePointDetailService.findUserPointRankingPage(page, workKnowledgeBasePoint).getList();
+
+            String fileName;
+            String[] headers;
+
+            switch (rankingType) {
+                case "2":
+                    fileName = "贡献积分排行榜" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+                    headers = new String[]{"排名", "姓名", "所属部门", "手机号", "贡献积分(总获取)", "贡献已兑换", "贡献剩余"};
+                    break;
+                case "1":
+                    fileName = "基础积分排行榜" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+                    headers = new String[]{"排名", "姓名", "所属部门", "手机号", "基础积分(总获取)", "基础已兑换", "基础剩余"};
+                    break;
+                default: // 0
+                    fileName = "积分排行榜" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+                    headers = new String[]{"排名", "姓名", "所属部门", "手机号",
+                            "基础积分(总获取)", "基础已兑换", "基础剩余",
+                            "贡献积分(总获取)", "贡献已兑换", "贡献剩余",
+                            "总积分"};
+                    break;
+            }
+
+            ExportExcel exportExcel = new ExportExcel(fileName, headers);
+
+            int rank = 1;
+            for (WorkKnowledgeBasePoint item : rankingList) {
+                int basicEarned = item.getBasicPoint() != null ? item.getBasicPoint() : 0;
+                int basicExchanged = item.getExchangedBasicPoint() != null ? item.getExchangedBasicPoint() : 0;
+                int basicRemaining = item.getRemainingBasicPoint() != null ? item.getRemainingBasicPoint() : 0;
+                int contribEarned = item.getContributionPoint() != null ? item.getContributionPoint() : 0;
+                int contribExchanged = item.getExchangedContributionPoint() != null ? item.getExchangedContributionPoint() : 0;
+                int contribRemaining = item.getRemainingContributionPoint() != null ? item.getRemainingContributionPoint() : 0;
+                int totalEarned = item.getTotalPoint() != null ? item.getTotalPoint() : 0;
+
+                org.apache.poi.ss.usermodel.Row row = exportExcel.addRow();
+                int col = 0;
+                exportExcel.addCell(row, col++, rank++, 2, Integer.class);
+                exportExcel.addCell(row, col++, item.getUserName(), 2, String.class);
+                exportExcel.addCell(row, col++, item.getOfficeName(), 2, String.class);
+                exportExcel.addCell(row, col++, item.getMobile() != null ? item.getMobile() : (item.getPhone() != null ? item.getPhone() : ""), 2, String.class);
+
+                switch (rankingType) {
+                    case "contribution":
+                        exportExcel.addCell(row, col++, contribEarned, 2, Integer.class);
+                        exportExcel.addCell(row, col++, contribExchanged, 2, Integer.class);
+                        exportExcel.addCell(row, col++, contribRemaining, 2, Integer.class);
+                        break;
+                    case "basic":
+                        exportExcel.addCell(row, col++, basicEarned, 2, Integer.class);
+                        exportExcel.addCell(row, col++, basicExchanged, 2, Integer.class);
+                        exportExcel.addCell(row, col++, basicRemaining, 2, Integer.class);
+                        break;
+                    default:
+                        exportExcel.addCell(row, col++, basicEarned, 2, Integer.class);
+                        exportExcel.addCell(row, col++, basicExchanged, 2, Integer.class);
+                        exportExcel.addCell(row, col++, basicRemaining, 2, Integer.class);
+                        exportExcel.addCell(row, col++, contribEarned, 2, Integer.class);
+                        exportExcel.addCell(row, col++, contribExchanged, 2, Integer.class);
+                        exportExcel.addCell(row, col++, contribRemaining, 2, Integer.class);
+                        exportExcel.addCell(row, col++, totalEarned, 2, Integer.class);
+                        break;
+                }
+            }
+
+            exportExcel.write(response, fileName).dispose();
+        } catch (Exception e) {
+            addMessage(redirectAttributes, "导出积分排行榜失败:" + e.getMessage());
+        }
+    }
+
 }

+ 5 - 1
src/main/java/com/jeeplus/modules/ruralprojectrecords/service/RuralProjectMessageElectronicSealService.java

@@ -3808,7 +3808,11 @@ public class RuralProjectMessageElectronicSealService extends CrudService<RuralP
                     filePath.mkdirs();
                 }
                 new OSSClientUtil().downByStreamSaveLocal(ossKey,fileName,path+fileName);
-                imageStr = ImageUtil.getImageStr(path+fileName);
+                // 对签字章图片进行预处理(去阴影、去背景、裁剪白边)
+                ImageUtil.SignImageResult result = ImageUtil.processSignImage(path + fileName);
+                if (result != null) {
+                    imageStr = result.getBase64();
+                }
             }
         }catch (Exception e){
             e.getMessage();

+ 5 - 1
src/main/java/com/jeeplus/modules/ruralprojectrecords/service/changZhouSignature/RuralProjectMessageElectronicSealCZService.java

@@ -2522,7 +2522,11 @@ public class RuralProjectMessageElectronicSealCZService extends CrudService<Rura
                     filePath.mkdirs();
                 }
                 new OSSClientUtil().downByStreamSaveLocal(ossKey,fileName,path+fileName);
-                imageStr = ImageUtil.getImageStr(path+fileName);
+                // 对签字章图片进行预处理(去阴影、去背景、裁剪白边)
+                ImageUtil.SignImageResult result = ImageUtil.processSignImage(path + fileName);
+                if (result != null) {
+                    imageStr = result.getBase64();
+                }
             }
         }catch (Exception e){
             e.getMessage();

+ 5 - 1
src/main/java/com/jeeplus/modules/ruralprojectrecords/service/yanChengSignature/RuralProjectMessageElectronicSealYCService.java

@@ -2508,7 +2508,11 @@ public class RuralProjectMessageElectronicSealYCService extends CrudService<Rura
                     filePath.mkdirs();
                 }
                 new OSSClientUtil().downByStreamSaveLocal(ossKey,fileName,path+fileName);
-                imageStr = ImageUtil.getImageStr(path+fileName);
+                // 对签字章图片进行预处理(去阴影、去背景、裁剪白边)
+                ImageUtil.SignImageResult result = ImageUtil.processSignImage(path + fileName);
+                if (result != null) {
+                    imageStr = result.getBase64();
+                }
             }
         }catch (Exception e){
             e.getMessage();

+ 5 - 1
src/main/java/com/jeeplus/modules/workfullmanage/service/WorkFullManageService.java

@@ -959,7 +959,11 @@ public class WorkFullManageService extends CrudService<WorkFullManageDao, WorkFu
 					filePath.mkdirs();
 				}
 				new OSSClientUtil().downByStreamSaveLocal(ossKey,fileName,path+fileName);
-				imageStr = ImageUtil.getImageStr(path+fileName);
+				// 对签字章图片进行预处理(去阴影、去背景、二值化、裁剪白边、归一化尺寸)
+				ImageUtil.SignImageResult result = ImageUtil.processSignImage(path + fileName);
+				if (result != null) {
+					imageStr = result.getBase64();
+				}
 			}
 		}catch (Exception e){
 			e.getMessage();

+ 1 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseClickRecordDao.xml

@@ -138,6 +138,7 @@
         WHERE a.file_id = #{fileId}
           AND a.del_flag = '0'
         ORDER BY a.click_count DESC, a.last_click_time DESC
+        LIMIT #{offset}, #{limit}
     </select>
 
 </mapper>

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

@@ -133,6 +133,14 @@
           AND a.del_flag = 0
     </select>
 
+    <!-- 统计文件的点赞记录总数(含已取消,用于详情页分页) -->
+    <select id="countAllLikesByFileId" resultType="java.lang.Integer">
+        SELECT COUNT(DISTINCT a.user_id)
+        FROM work_knowledge_base_like_record a
+        WHERE a.file_id = #{fileId}
+          AND a.del_flag = 0
+    </select>
+
     <!-- 查询文件的点赞详情列表(不分页) -->
     <select id="findLikeDetailList" resultType="WorkKnowledgeBaseLikeRecord">
         SELECT
@@ -163,6 +171,7 @@
         WHERE a.file_id = #{fileId}
           AND a.del_flag = 0
         ORDER BY a.like_time DESC
+        LIMIT #{offset}, #{limit}
     </select>
 
     <!-- 查询所有已被点赞过(status=1)的文档 ID(去重) -->

+ 217 - 5
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml

@@ -268,8 +268,8 @@
             <if test="ruleType != null">
                 AND r.rule_type = #{ruleType}
             </if>
-            <!-- 按积分分类节点及其所有子节点筛选,始终包含扣分标准(id=10)的记录 -->
-            <if test="columnId != null and columnId != ''">
+            <!-- 按积分分类节点及其所有子节点筛选,始终包含扣分标准(id=10)的记录;columnId='all'时不做分类过滤 -->
+            <if test="columnId != null and columnId != '' and columnId != 'all'">
                 AND (d.category_id = #{columnId} OR c.parent_ids LIKE CONCAT('%,', #{columnId}, ',%') OR d.category_id = '10')
             </if>
             <!-- 正确日期查询:使用 DATE() 函数 + 直接传 Date 类型 -->
@@ -315,8 +315,8 @@
             <if test="ruleType != null">
                 AND r.rule_type = #{ruleType}
             </if>
-            <!-- 按积分分类节点及其所有子节点筛选,始终包含扣分标准(id=10)的记录 -->
-            <if test="columnId != null and columnId != ''">
+            <!-- 按积分分类节点及其所有子节点筛选,始终包含扣分标准(id=10)的记录;columnId='all'时不做分类过滤 -->
+            <if test="columnId != null and columnId != '' and columnId != 'all'">
                 AND (d.category_id = #{columnId} OR c.parent_ids LIKE CONCAT('%,', #{columnId}, ',%') OR d.category_id = '10')
             </if>
             <!-- 正确日期查询:使用 DATE() 函数 + 直接传 Date 类型 -->
@@ -344,7 +344,7 @@
         <where>
             d.del_flag = '0'
             AND d.user_id = #{userId}
-            <if test="columnId != null and columnId != ''">
+            <if test="columnId != null and columnId != '' and columnId != 'all'">
                 AND (d.category_id = #{columnId} OR c.parent_ids LIKE CONCAT('%,', #{columnId}, ',%') OR d.category_id = '10')
             </if>
         </where>
@@ -927,4 +927,216 @@
           AND user_id = #{userId}
     </select>
 
+    <!-- ============================================================
+         积分排行榜查询(同时返回基础积分和贡献积分的获取、兑换、剩余)
+         获取积分 = 只累加正数积分(不算兑换扣分)
+         已兑换 = 负数积分的绝对值
+         剩余 = 获取 - 已兑换
+         总积分 = 基础获取 + 贡献获取
+         ============================================================ -->
+    <select id="findUserPointRankingList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint">
+        SELECT
+            u.id AS "userId",
+            u.name AS "userName",
+            u.office_id AS "officeId",
+            CASE WHEN u.user_type = '1' THEN '系统' ELSE o.name END AS "officeName",
+            COALESCE(u.mobile, u.phone) AS "mobile",
+            COALESCE(SUM(CASE WHEN (d.category_id = '1' OR c.parent_ids LIKE '%,1,%') AND d.point > 0 THEN d.point ELSE 0 END), 0) AS "basicPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '1' OR c.parent_ids LIKE '%,1,%') AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedBasicPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '1' OR c.parent_ids LIKE '%,1,%') THEN d.point ELSE 0 END), 0) AS "remainingBasicPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '2' OR c.parent_ids LIKE '%,2,%' OR d.category_id = '10') AND d.point > 0 THEN d.point ELSE 0 END), 0) AS "contributionPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '2' OR c.parent_ids LIKE '%,2,%' OR d.category_id = '10') AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedContributionPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '2' OR c.parent_ids LIKE '%,2,%' OR d.category_id = '10') THEN d.point ELSE 0 END), 0) AS "remainingContributionPoint",
+            COALESCE(SUM(CASE WHEN d.point > 0 THEN d.point ELSE 0 END), 0) AS "totalPoint"
+        FROM sys_user u
+        LEFT JOIN work_knowledge_base_point_detail d ON d.user_id = u.id AND d.del_flag = '0'
+        LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+        LEFT JOIN sys_office o ON o.id = u.office_id AND o.del_flag = '0'
+        WHERE u.del_flag = '0'
+        GROUP BY u.id, u.name, u.office_id, o.name, u.mobile, u.phone, u.user_type
+        HAVING totalPoint > 0
+        ORDER BY totalPoint DESC
+    </select>
+
+    <!-- ============================================================
+         积分排行榜分页查询(支持人员/部门/日期筛选)
+         ============================================================ -->
+    <select id="findUserPointRankingPage" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint">
+        SELECT
+            u.id AS "userId",
+            u.name AS "userName",
+            u.office_id AS "officeId",
+            CASE WHEN u.user_type = '1' THEN '系统' ELSE o.name END AS "officeName",
+            COALESCE(u.mobile, u.phone) AS "mobile",
+            COALESCE(SUM(CASE WHEN (d.category_id = '1' OR c.parent_ids LIKE '%,1,%') AND d.point > 0 THEN d.point ELSE 0 END), 0) AS "basicPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '1' OR c.parent_ids LIKE '%,1,%') AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedBasicPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '1' OR c.parent_ids LIKE '%,1,%') THEN d.point ELSE 0 END), 0) AS "remainingBasicPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '2' OR c.parent_ids LIKE '%,2,%' OR d.category_id = '10') AND d.point > 0 THEN d.point ELSE 0 END), 0) AS "contributionPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '2' OR c.parent_ids LIKE '%,2,%' OR d.category_id = '10') AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedContributionPoint",
+            COALESCE(SUM(CASE WHEN (d.category_id = '2' OR c.parent_ids LIKE '%,2,%' OR d.category_id = '10') THEN d.point ELSE 0 END), 0) AS "remainingContributionPoint",
+            COALESCE(SUM(CASE WHEN d.point > 0 THEN d.point ELSE 0 END), 0) AS "totalPoint"
+        FROM sys_user u
+        LEFT JOIN work_knowledge_base_point_detail d ON d.user_id = u.id AND d.del_flag = '0'
+        LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+        LEFT JOIN sys_office o ON o.id = u.office_id AND o.del_flag = '0'
+        WHERE u.del_flag = '0'
+        <if test="submitterId != null and submitterId != ''">
+            AND u.id = #{submitterId}
+        </if>
+        <if test="officeId != null and officeId != ''">
+            AND u.office_id = #{officeId}
+        </if>
+        <if test="gainBeginDate != null">
+            AND d.create_date &gt;= #{gainBeginDate}
+        </if>
+        <if test="gainEndDate != null">
+            AND d.create_date &lt;= TIMESTAMP(#{gainEndDate}, '23:59:59')
+        </if>
+        GROUP BY u.id, u.name, u.office_id, o.name, u.mobile, u.phone, u.user_type
+        HAVING totalPoint > 0
+        ORDER BY 
+            CASE 
+                WHEN #{columnId} = '1' THEN COALESCE(SUM(CASE WHEN (d.category_id = '1' OR c.parent_ids LIKE '%,1,%') AND d.point > 0 THEN d.point ELSE 0 END), 0)
+                WHEN #{columnId} = '2' THEN COALESCE(SUM(CASE WHEN (d.category_id = '2' OR c.parent_ids LIKE '%,2,%' OR d.category_id = '10') AND d.point > 0 THEN d.point ELSE 0 END), 0)
+                ELSE COALESCE(SUM(CASE WHEN d.point > 0 THEN d.point ELSE 0 END), 0)
+            END DESC
+    </select>
+
+    <!-- 积分排行榜分页总数 -->
+    <select id="findUserPointRankingCount" resultType="int">
+        SELECT COUNT(*) FROM (
+            SELECT u.id
+            FROM sys_user u
+            LEFT JOIN work_knowledge_base_point_detail d ON d.user_id = u.id AND d.del_flag = '0'
+            LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+            WHERE u.del_flag = '0'
+            <if test="submitterId != null and submitterId != ''">
+                AND u.id = #{submitterId}
+            </if>
+            <if test="officeId != null and officeId != ''">
+                AND u.office_id = #{officeId}
+            </if>
+            <if test="gainBeginDate != null">
+                AND d.create_date &gt;= #{gainBeginDate}
+            </if>
+            <if test="gainEndDate != null">
+                AND d.create_date &lt;= TIMESTAMP(#{gainEndDate}, '23:59:59')
+            </if>
+            GROUP BY u.id
+            HAVING COALESCE(SUM(CASE WHEN d.point > 0 THEN d.point ELSE 0 END), 0) > 0
+        ) tmp
+    </select>
+
+    <!-- ============================================================
+         兑换历史汇总查询(按用户汇总,不按月份拆分)
+         基础积分兑换: category_id='1', 贡献积分兑换: category_id='2'
+         change_type=9 为兑换扣减记录,point 为负数
+         每个用户一行,展示基础兑换/贡献兑换/总兑换
+         支持按月份筛选(exchangeMonth 可选,格式 2026-10)
+         ============================================================ -->
+    <select id="findExchangeHistoryList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint">
+        SELECT
+            u.id AS "userId",
+            u.name AS "userName",
+            u.office_id AS "officeId",
+            CASE WHEN u.user_type = '1' THEN '系统' ELSE o.name END AS "officeName",
+            COALESCE(u.mobile, u.phone) AS "mobile",
+            COALESCE(SUM(CASE WHEN d.category_id = '1' AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedBasicPoint",
+            COALESCE(SUM(CASE WHEN d.category_id = '2' AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedContributionPoint",
+            COALESCE(SUM(CASE WHEN d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangePoint"
+        FROM work_knowledge_base_point_detail d
+        INNER JOIN sys_user u ON u.id = d.user_id AND u.del_flag = '0'
+        LEFT JOIN sys_office o ON o.id = u.office_id AND o.del_flag = '0'
+        WHERE d.del_flag = '0'
+          AND d.change_type = 9
+          AND d.point &lt; 0
+          AND d.category_id IN ('1', '2')
+        <if test="exchangeMonth != null and exchangeMonth != ''">
+          AND DATE_FORMAT(d.create_date, '%Y-%m') = #{exchangeMonth}
+        </if>
+        GROUP BY u.id, u.name, u.office_id, o.name, u.mobile, u.phone, u.user_type
+        ORDER BY COALESCE(SUM(CASE WHEN d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) DESC
+    </select>
+
+    <!-- ============================================================
+         获取所有有兑换记录的月份列表(去重,倒序)
+         ============================================================ -->
+    <select id="findExchangeMonths" resultType="java.lang.String">
+        SELECT DISTINCT DATE_FORMAT(create_date, '%Y-%m') AS exchangeMonth
+        FROM work_knowledge_base_point_detail
+        WHERE del_flag = '0'
+          AND change_type = 9
+          AND point &lt; 0
+          AND category_id IN ('1', '2')
+        ORDER BY exchangeMonth DESC
+    </select>
+
+    <!-- ============================================================
+         兑换历史分页查询(支持人员/部门/月份筛选)
+         ============================================================ -->
+    <select id="findExchangeHistoryPage" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint">
+        SELECT
+            u.id AS "userId",
+            u.name AS "userName",
+            u.office_id AS "officeId",
+            CASE WHEN u.user_type = '1' THEN '系统' ELSE o.name END AS "officeName",
+            COALESCE(u.mobile, u.phone) AS "mobile",
+            COALESCE(SUM(CASE WHEN d.category_id = '1' AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedBasicPoint",
+            COALESCE(SUM(CASE WHEN d.category_id = '2' AND d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangedContributionPoint",
+            COALESCE(SUM(CASE WHEN d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) AS "exchangePoint"
+        FROM work_knowledge_base_point_detail d
+        INNER JOIN sys_user u ON u.id = d.user_id AND u.del_flag = '0'
+        LEFT JOIN sys_office o ON o.id = u.office_id AND o.del_flag = '0'
+        WHERE d.del_flag = '0'
+          AND d.change_type = 9
+          AND d.point &lt; 0
+          AND d.category_id IN ('1', '2')
+        <if test="submitterId != null and submitterId != ''">
+            AND u.id = #{submitterId}
+        </if>
+        <if test="officeId != null and officeId != ''">
+            AND u.office_id = #{officeId}
+        </if>
+        <if test="exchangeMonth != null and exchangeMonth != ''">
+            AND DATE_FORMAT(d.create_date, '%Y-%m') = #{exchangeMonth}
+        </if>
+        <if test="columnId == '1'">
+            AND d.category_id = '1'
+        </if>
+        <if test="columnId == '2'">
+            AND d.category_id = '2'
+        </if>
+        GROUP BY u.id, u.name, u.office_id, o.name, u.mobile, u.phone, u.user_type
+        HAVING 
+            <choose>
+                <when test="columnId == '1'">exchangedBasicPoint > 0</when>
+                <when test="columnId == '2'">exchangedContributionPoint > 0</when>
+                <otherwise>exchangePoint > 0</otherwise>
+            </choose>
+        ORDER BY COALESCE(SUM(CASE WHEN d.point &lt; 0 THEN ABS(d.point) ELSE 0 END), 0) DESC
+    </select>
+
+    <!-- 兑换历史分页总数 -->
+    <select id="findExchangeHistoryCount" resultType="int">
+        SELECT COUNT(*) FROM (
+            SELECT u.id
+            FROM work_knowledge_base_point_detail d
+            INNER JOIN sys_user u ON u.id = d.user_id AND u.del_flag = '0'
+            WHERE d.del_flag = '0'
+              AND d.change_type = 9
+              AND d.point &lt; 0
+              AND d.category_id IN ('1', '2')
+            <if test="submitterId != null and submitterId != ''">
+                AND u.id = #{submitterId}
+            </if>
+            <if test="officeId != null and officeId != ''">
+                AND u.office_id = #{officeId}
+            </if>
+            <if test="exchangeMonth != null and exchangeMonth != ''">
+                AND DATE_FORMAT(d.create_date, '%Y-%m') = #{exchangeMonth}
+            </if>
+            GROUP BY u.id
+        ) tmp
+    </select>
+
 </mapper>

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

@@ -169,6 +169,7 @@
         WHERE a.file_id = #{fileId}
           AND a.del_flag = 0
         ORDER BY a.first_read_time DESC
+        LIMIT #{offset}, #{limit}
     </select>
 
     <!-- 查询所有已被阅读过的文档 ID(去重) -->

+ 4 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseClickDetail.jsp

@@ -11,6 +11,10 @@
     </script>
 </head>
 <body>
+<form:form id="searchForm" modelAttribute="page" action="${ctx}/workKnowledgeBase/readLike/clickDetailPage?fileId=${fileId}" method="post" style="display:none;">
+    <input type="hidden" name="pageNo" id="pageNo" value="${page.pageNo}"/>
+    <input type="hidden" name="pageSize" id="pageSize" value="${page.pageSize}"/>
+</form:form>
 <div class="wrapper wrapper-content">
     <div class="layui-row">
         <div class="full-width fl">

+ 4 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseLikeDetail.jsp

@@ -11,6 +11,10 @@
     </script>
 </head>
 <body>
+<form:form id="searchForm" modelAttribute="page" action="${ctx}/workKnowledgeBase/readLike/likeDetailPage?fileId=${fileId}" method="post" style="display:none;">
+    <input type="hidden" name="pageNo" id="pageNo" value="${page.pageNo}"/>
+    <input type="hidden" name="pageSize" id="pageSize" value="${page.pageSize}"/>
+</form:form>
 <div class="wrapper wrapper-content">
     <div class="layui-row">
         <div class="full-width fl">

+ 244 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointExchangeHistory.jsp

@@ -0,0 +1,244 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ include file="/webpage/include/taglib.jsp"%>
+<html>
+<head>
+	<title>已兑换记录</title>
+	<meta name="decorator" content="default"/>
+	<script type="text/javascript">
+		$(document).ready(function() {
+			// 初始化获取时间 laydate 日期选择器
+			layui.use('laydate', function() {
+				var laydate = layui.laydate;
+				laydate.render({ elem: '#gainBeginDate', event: 'focus', type: 'date', format: 'yyyy-MM-dd', trigger: 'click' });
+				laydate.render({ elem: '#gainEndDate', event: 'focus', type: 'date', format: 'yyyy-MM-dd', trigger: 'click' });
+			});
+
+			// 页签切换
+			$(".ex-tabs li").each(function(){
+				$(this).removeClass("active");
+				if($(this).attr('value') == $('#currentExType').val()){
+					$(this).addClass("active");
+				}
+			});
+			$(".ex-tabs li").click(function(){
+				$(".ex-tabs li").removeClass("active");
+				$(this).addClass("active");
+				var type = $(this).attr('value');
+				$('#currentExType').val(type);
+				$('#columnId').val(type);
+				search();
+			});
+		});
+
+		function search() {
+			$('#searchForm').submit();
+			return false;
+		}
+
+		function resetSearch() {
+			$('#searchForm div.query input[type=text]').val('');
+			$('#searchForm div.query input[type=hidden]').val('');
+			$('#searchForm div.query select').val('');
+			$('#monthFilter').val('');
+			$('#currentExType').val('0');
+			$('.ex-tabs li').removeClass('active');
+			$('.ex-tabs li[value="0"]').addClass('active');
+			$('#searchForm').submit();
+		}
+	</script>
+</head>
+<body>
+<div class="wrapper wrapper-content">
+	<sys:message content="${message}"/>
+
+	<!-- 页签 -->
+	<div class="list-form-tab contentShadow shadowLTR" id="tabDiv">
+		<ul class="ex-tabs list-tabs">
+			<li value="0" class="active"><a>全部</a></li>
+			<li value="2"><a>贡献积分</a></li>
+			<li value="1"><a>基础积分</a></li>
+		</ul>
+	</div>
+	<input type="hidden" id="currentExType" value="${not empty workKnowledgeBasePoint.columnId ? workKnowledgeBasePoint.columnId : '0'}"/>
+
+	<div class="layui-row">
+		<!-- 查询条件 -->
+		<div class="full-width fl">
+			<div class="contentShadow layui-row" id="queryDiv">
+				<form:form id="searchForm" modelAttribute="workKnowledgeBasePoint" action="${ctx}/workKnowledgeBase/point/exchangeHistoryPage" method="post" class="form-inline">
+					<input id="pageNo" name="pageNo" type="hidden" value="${page.pageNo}"/>
+					<input id="pageSize" name="pageSize" type="hidden" value="${page.pageSize}"/>
+					<input id="columnId" name="columnId" type="hidden" value="${not empty workKnowledgeBasePoint.columnId ? workKnowledgeBasePoint.columnId : '0'}"/>
+					<div class="commonQuery lw7">
+						<div class="layui-item query athird" style="width: 20%">
+							<label class="layui-form-label">人员名称:</label>
+							<div class="layui-input-block with-icon">
+								<sys:inquireselectUserNotReadolnyTow id="submitter" name="submitterId" value="${workKnowledgeBasePoint.submitterId}" labelName="submitterName" labelValue="${workKnowledgeBasePoint.submitterName}" cssStyle="background-color: #fff"
+																		 title="用户" url="/sys/office/treeDataAll?type=3" cssClass="form-control layui-input" allowClear="true" notAllowSelectParent="true"/>
+							</div>
+						</div>
+						<div class="layui-item query athird" style="width: 20%">
+							<label class="layui-form-label">人员部门:</label>
+							<div class="layui-input-block with-icon">
+								<sys:treeselect id="officeId" name="officeId" value="${workKnowledgeBasePoint.officeId}" labelName="officeName" labelValue="${workKnowledgeBasePoint.officeName}" cssStyle="background-color: #fff"
+												title="部门" url="/sys/office/treeDataAll?type=6" cssClass="form-control layui-input" allowInput="true" allowClear="true" notAllowSelectParent="true" />
+							</div>
+						</div>
+						<div class="layui-item query athird" style="width: 20%">
+							<label class="layui-form-label">兑换月份:</label>
+							<div class="layui-input-block">
+								<select id="monthFilter" name="exchangeMonth" class="form-control layui-input" style="background-color: #fff;">
+									<option value="">全部月份</option>
+									<c:forEach items="${exchangeMonths}" var="m">
+										<option value="${m}" ${workKnowledgeBasePoint.exchangeMonth == m ? 'selected' : ''}>${m}</option>
+									</c:forEach>
+								</select>
+							</div>
+						</div>
+						<div class="layui-item query athird" style="width: 20%">
+							<label class="layui-form-label">获取时间:</label>
+							<div class="layui-input-block">
+								<input id="gainBeginDate" name="gainBeginDate" placeholder="开始时间" type="text" readonly="readonly" maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+								   value="<fmt:formatDate value='${workKnowledgeBasePoint.gainBeginDate}' pattern='yyyy-MM-dd'/>"/>
+								<span class="group-sep">-</span>
+								<input id="gainEndDate" name="gainEndDate" placeholder="结束时间" type="text" readonly="readonly" maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+								   value="<fmt:formatDate value='${workKnowledgeBasePoint.gainEndDate}' pattern='yyyy-MM-dd'/>"/>
+							</div>
+						</div>
+						<div class="layui-item athird" style="width: 20%">
+							<div class="input-group">
+								<div class="layui-btn-group search-spacing">
+									<button id="searchQuery" class="layui-btn layui-btn-sm layui-bg-blue" onclick="return search()">查询</button>
+									<button id="searchReset" type="button" class="layui-btn layui-btn-sm" onclick="resetSearch()">重置</button>
+								</div>
+							</div>
+						</div>
+						<div style="clear:both;"></div>
+					</div>
+				</form:form>
+			</div>
+		</div>
+
+		<!-- 表格 + 分页 -->
+		<div class="full-width fl">
+			<div class="contentShadow layui-form contentDetails">
+				<div class="nav-btns">
+					<div class="layui-btn-group">
+						<button type="button" class="layui-btn layui-btn-sm layui-bg-blue" onclick="exportExchangeHistory()">
+							<i class="layui-icon">&#xe67d;</i> 导出当前列表
+						</button>
+					</div>
+					<div style="clear: both;"></div>
+				</div>
+				<table class="oa-table layui-table" id="contentTable"></table>
+				<table:page page="${page}"></table:page>
+				<div style="clear:both;"></div>
+			</div>
+		</div>
+	</div>
+</div>
+<script src="${ctxStatic}/layer-v2.3/layui/layui.all.js" charset="utf-8"></script>
+<script>
+    var exType = $('#currentExType').val() || '0';
+
+    layui.use('table', function(){
+        var cols;
+        if (exType === '2') {
+            cols = [[
+                {field:'index', align:'center', title: '序号', width:60}
+                ,{field:'userName', align: 'center', title: '姓名', minWidth: 120}
+                ,{field:'officeName', align: 'center', title: '所属部门', minWidth: 160}
+                ,{field:'mobile', align: 'center', title: '手机号', minWidth: 130, templet: function(d) { return d.mobile || d.phone || ''; }}
+                ,{field:'exchangedContributionPoint', align: 'center', title: '贡献积分兑换', width: 130, templet: function(d) {
+                    return '<span style="color:#1565c0;font-weight:bold;">' + (d.exchangedContributionPoint || 0) + '</span>';
+                }}
+            ]];
+        } else if (exType === '1') {
+            cols = [[
+                {field:'index', align:'center', title: '序号', width:60}
+                ,{field:'userName', align: 'center', title: '姓名', minWidth: 120}
+                ,{field:'officeName', align: 'center', title: '所属部门', minWidth: 160}
+                ,{field:'mobile', align: 'center', title: '手机号', minWidth: 130, templet: function(d) { return d.mobile || d.phone || ''; }}
+                ,{field:'exchangedBasicPoint', align: 'center', title: '基础积分兑换', width: 130, templet: function(d) {
+                    return '<span style="color:#2e7d32;font-weight:bold;">' + (d.exchangedBasicPoint || 0) + '</span>';
+                }}
+            ]];
+        } else {
+            cols = [[
+                {field:'index', align:'center', title: '序号', width:60}
+                ,{field:'userName', align: 'center', title: '姓名', minWidth: 120}
+                ,{field:'officeName', align: 'center', title: '所属部门', minWidth: 160}
+                ,{field:'mobile', align: 'center', title: '手机号', minWidth: 130, templet: function(d) { return d.mobile || d.phone || ''; }}
+                ,{field:'exchangedBasicPoint', align: 'center', title: '基础积分兑换', width: 120, templet: function(d) {
+                    return '<span style="color:#2e7d32;font-weight:bold;">' + (d.exchangedBasicPoint || 0) + '</span>';
+                }}
+                ,{field:'exchangedContributionPoint', align: 'center', title: '贡献积分兑换', width: 130, templet: function(d) {
+                    return '<span style="color:#1565c0;font-weight:bold;">' + (d.exchangedContributionPoint || 0) + '</span>';
+                }}
+                ,{field:'exchangePoint', align: 'center', title: '总兑换积分', width: 110, templet: function(d) {
+                    return '<span style="color:#e65100;font-weight:bold;font-size:15px;">' + (d.exchangePoint || 0) + '</span>';
+                }}
+            ]];
+        }
+
+        layui.table.render({
+            limit:${ page.pageSize }
+            ,elem: '#contentTable'
+            ,page: false
+            ,cols: cols
+            ,data: [
+                <c:choose>
+                    <c:when test="${not empty page.list}">
+                        <c:forEach items="${page.list}" var="row" varStatus="st">
+                            <c:if test="${st.index != 0}">,</c:if>
+                            {
+                                "index": "${st.index + 1 + (page.pageNo - 1) * page.pageSize}"
+                                ,"userId": "${row.userId}"
+                                ,"userName": "<c:out value='${row.userName}'/>"
+                                ,"officeName": "<c:out value='${row.officeName}'/>"
+                                ,"mobile": "<c:out value='${row.mobile}'/>"
+                                ,"phone": "<c:out value='${row.phone}'/>"
+                                ,"exchangedBasicPoint": "${row.exchangedBasicPoint}"
+                                ,"exchangedContributionPoint": "${row.exchangedContributionPoint}"
+                                ,"exchangePoint": "${row.exchangePoint}"
+                            }
+                        </c:forEach>
+                    </c:when>
+                    <c:otherwise></c:otherwise>
+                </c:choose>
+            ]
+        });
+    });
+
+    function exportExchangeHistory() {
+        var type = $('#currentExType').val() || 'all';
+        var submitterId = $('input[name="submitterId"]').val() || '';
+        var officeId = $('input[name="officeId"]').val() || '';
+        var exchangeMonth = $('#monthFilter').val() || '';
+        var gainBeginDate = $('#gainBeginDate').val() || '';
+        var gainEndDate = $('#gainEndDate').val() || '';
+        var randomId = "down_" + new Date().getTime();
+        var downForm = $('<form>', {
+            'action': '${ctx}/workKnowledgeBase/point/exportExchangeHistory',
+            'method': 'GET',
+            'style': 'display:none;'
+        }).appendTo('body');
+        $('<input>').attr({ type: 'hidden', name: 'exType', value: type }).appendTo(downForm);
+        if (submitterId) $('<input>').attr({ type: 'hidden', name: 'submitterId', value: submitterId }).appendTo(downForm);
+        if (officeId) $('<input>').attr({ type: 'hidden', name: 'officeId', value: officeId }).appendTo(downForm);
+        if (exchangeMonth) $('<input>').attr({ type: 'hidden', name: 'exchangeMonth', value: exchangeMonth }).appendTo(downForm);
+        if (gainBeginDate) $('<input>').attr({ type: 'hidden', name: 'gainBeginDate', value: gainBeginDate }).appendTo(downForm);
+        if (gainEndDate) $('<input>').attr({ type: 'hidden', name: 'gainEndDate', value: gainEndDate }).appendTo(downForm);
+        var downIframe = $('<iframe>', { 'name': randomId, 'style': 'display:none;' }).appendTo('body');
+        downForm.attr('target', randomId);
+        downIframe.on('load', function() {
+            try { downForm.remove(); downIframe.remove(); } catch(e) {}
+        });
+        downForm.submit();
+        setTimeout(function() {
+            try { downForm.remove(); downIframe.remove(); } catch(e) {}
+        }, 3000);
+    }
+</script>
+</body>
+</html>

+ 36 - 2
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointList.jsp

@@ -21,7 +21,7 @@
 			var columnId = $("#columnId").val();
 			$(".list-tabs li").each(function(){
 				$(this).removeClass("active");
-				if($(this).val() == columnId){
+				if($(this).attr('value') == columnId){
 					$(this).addClass("active");
 				}
 			});
@@ -29,7 +29,7 @@
 			$(".list-tabs li").click(function(){
 				$(".list-tabs li").removeClass("active");
 				$(this).addClass("active");
-				$("#columnId").val($(this).val());
+				$("#columnId").val($(this).attr('value'));
 				$("#searchForm").submit();
 			});
 
@@ -190,6 +190,12 @@
 						<shiro:hasPermission name="workKnowledgeBase:point:cancelExchange">
 							<button type="button" data-toggle="tooltip" data-placement="top" class="layui-btn layui-btn-sm layui-btn-danger" id="cancelLastExchangeBtn" title="导出文档出现问题时,可撤销最近一次兑换"> 取消上次兑换</button>
 						</shiro:hasPermission>
+						<shiro:hasPermission name="workKnowledgeBase:point:listAll">
+							<button type="button" data-toggle="tooltip" data-placement="top" class="layui-btn layui-btn-sm layui-bg-green" id="rankingBtn" title="查看所有人积分排行(基础+贡献)"> 积分排行榜</button>
+						</shiro:hasPermission>
+						<%--<shiro:hasPermission name="workKnowledgeBase:point:listAll">
+							<button type="button" data-toggle="tooltip" data-placement="top" class="layui-btn layui-btn-sm layui-bg-orange" id="exchangeHistoryBtn" title="查看所有人的历史兑换记录"> 已兑换记录</button>
+						</shiro:hasPermission>--%>
 					</div>
 					<div style="float: right;margin-right: 30px;">
 						<label class="layui-form-label double-line">排序规则:</label>
@@ -329,6 +335,34 @@
         return false;
     });
 
+    // 积分排行榜(弹出框)
+    $('#rankingBtn').click(function(){
+        top.layer.open({
+            type: 2,
+            area: ['85%', '80%'],
+            title: '积分排行榜',
+            maxmin: true,
+            content: '${ctx}/workKnowledgeBase/point/rankingPage',
+            btn: ['关闭'],
+            btn1: function(index) { top.layer.close(index); }
+        });
+        return false;
+    });
+
+    // 已兑换记录(弹出框)
+    $('#exchangeHistoryBtn').click(function(){
+        top.layer.open({
+            type: 2,
+            area: ['85%', '80%'],
+            title: '已兑换记录',
+            maxmin: true,
+            content: '${ctx}/workKnowledgeBase/point/exchangeHistoryPage',
+            btn: ['关闭'],
+            btn1: function(index) { top.layer.close(index); }
+        });
+        return false;
+    });
+
     // 取消上次兑换(导出文档出现问题时的回退操作)
     $('#cancelLastExchangeBtn').click(function(){
         top.layer.confirm('确定取消上次兑换吗?<br><span style="color:#999;">将删除最近一次兑换的扣减记录并恢复积分,仅在导出文档出现问题时使用。</span>', {icon: 3, title: '取消上次兑换'}, function(confirmIndex){

+ 258 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointRanking.jsp

@@ -0,0 +1,258 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ include file="/webpage/include/taglib.jsp"%>
+<html>
+<head>
+	<title>积分排行榜</title>
+	<meta name="decorator" content="default"/>
+	<script type="text/javascript">
+		$(document).ready(function() {
+			// 初始化获取时间 laydate 日期选择器
+			layui.use('laydate', function() {
+				var laydate = layui.laydate;
+				laydate.render({ elem: '#gainBeginDate', event: 'focus', type: 'date', format: 'yyyy-MM-dd', trigger: 'click' });
+				laydate.render({ elem: '#gainEndDate', event: 'focus', type: 'date', format: 'yyyy-MM-dd', trigger: 'click' });
+			});
+
+			var columnId = $("#columnId").val();
+			$(".list-tabs li").each(function(){
+				$(this).removeClass("active");
+				if($(this).attr('value') == columnId){
+					$(this).addClass("active");
+				}
+			});
+
+			$(".list-tabs li").click(function(){
+				$(".list-tabs li").removeClass("active");
+				$(this).addClass("active");
+				$("#columnId").val($(this).attr('value'));
+				$("#searchForm").submit();
+			});
+		});
+
+		function search() {
+			$('#searchForm').submit();
+			return false;
+		}
+
+		function resetSearch() {
+			var currentColumnId = $("#columnId").val();
+			$('#searchForm div.query input[type=text]').val('');
+			$('#searchForm div.query input[type=hidden]').val('');
+			$('#searchForm div.query select').val('');
+			$("#columnId").val(currentColumnId);
+			$('#searchForm').submit();
+		}
+
+		function openUserDetail(userId, userName) {
+			var columnId = $("#columnId").val() || '0';
+			var columnNames = {'0': '全部积分', '1': '基础积分', '2': '贡献积分'};
+			var typeName = columnNames[columnId] || '积分';
+			var url = '${ctx}/workKnowledgeBase/point/userDetail?userId=' + userId + '&userName=' + encodeURIComponent(userName || '') + '&columnId=' + columnId;
+			top.layer.open({
+				type: 2, area: ['85%', '80%'], title: (userName || '用户') + ' - ' + typeName + '明细',
+				maxmin: true, content: url, btn: ['关闭'], btn1: function(index) { top.layer.close(index); }
+			});
+		}
+	</script>
+</head>
+<body>
+<div class="wrapper wrapper-content">
+	<sys:message content="${message}"/>
+
+	<div class="list-form-tab contentShadow shadowLTR" id="tabDiv">
+		<ul class="list-tabs" >
+			<li value="0" class="active"><a>全部</a></li>
+			<li value="2"><a>贡献积分</a></li>
+			<li value="1"><a>基础积分</a></li>
+		</ul>
+	</div>
+
+	<div class="layui-row">
+		<!-- 查询条件 -->
+		<div class="full-width fl">
+			<div class="contentShadow layui-row" id="queryDiv">
+				<form:form id="searchForm" modelAttribute="workKnowledgeBasePoint" action="${ctx}/workKnowledgeBase/point/rankingPage" method="post" class="form-inline">
+					<input id="pageNo" name="pageNo" type="hidden" value="${page.pageNo}"/>
+					<input id="pageSize" name="pageSize" type="hidden" value="${page.pageSize}"/>
+					<input id="columnId" name="columnId" type="hidden" value="${workKnowledgeBasePoint.columnId}"/>
+					<div class="commonQuery lw7">
+						<div class="layui-item query athird" style="width: 25%">
+							<label class="layui-form-label">人员名称:</label>
+							<div class="layui-input-block with-icon">
+								<sys:inquireselectUserNotReadolnyTow id="submitter" name="submitterId" value="${workKnowledgeBasePoint.submitterId}" labelName="submitterName" labelValue="${workKnowledgeBasePoint.submitterName}" cssStyle="background-color: #fff"
+																		 title="用户" url="/sys/office/treeDataAll?type=3" cssClass="form-control layui-input" allowClear="true" notAllowSelectParent="true"/>
+							</div>
+						</div>
+						<div class="layui-item query athird" style="width: 25%">
+							<label class="layui-form-label">人员部门:</label>
+							<div class="layui-input-block with-icon">
+								<sys:treeselect id="officeId" name="officeId" value="${workKnowledgeBasePoint.officeId}" labelName="officeName" labelValue="${workKnowledgeBasePoint.officeName}" cssStyle="background-color: #fff"
+												title="部门" url="/sys/office/treeDataAll?type=6" cssClass="form-control layui-input" allowInput="true" allowClear="true" notAllowSelectParent="true" />
+							</div>
+						</div>
+						<div class="layui-item query athird" style="width: 25%">
+							<label class="layui-form-label">获取时间:</label>
+							<div class="layui-input-block">
+								<input id="gainBeginDate" name="gainBeginDate" placeholder="开始时间" type="text" readonly="readonly" maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+								   value="<fmt:formatDate value='${workKnowledgeBasePoint.gainBeginDate}' pattern='yyyy-MM-dd'/>"/>
+								<span class="group-sep">-</span>
+								<input id="gainEndDate" name="gainEndDate" placeholder="结束时间" type="text" readonly="readonly" maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+								   value="<fmt:formatDate value='${workKnowledgeBasePoint.gainEndDate}' pattern='yyyy-MM-dd'/>"/>
+							</div>
+						</div>
+						<div class="layui-item athird" style="width: 25%">
+							<div class="input-group">
+								<div class="layui-btn-group search-spacing">
+									<button id="searchQuery" class="layui-btn layui-btn-sm layui-bg-blue" onclick="return search()">查询</button>
+									<button id="searchReset" type="button" class="layui-btn layui-btn-sm" onclick="resetSearch()">重置</button>
+								</div>
+							</div>
+						</div>
+						<div style="clear:both;"></div>
+					</div>
+				</form:form>
+			</div>
+		</div>
+
+		<!-- 表格 + 分页 -->
+		<div class="full-width fl">
+			<div class="contentShadow layui-form contentDetails">
+				<div class="nav-btns">
+					<div class="layui-btn-group">
+						<button type="button" class="layui-btn layui-btn-sm layui-bg-blue" onclick="exportRanking()">
+							<i class="layui-icon">&#xe67d;</i> 导出当前排行榜
+						</button>
+					</div>
+					<div style="clear: both;"></div>
+				</div>
+				<table class="oa-table layui-table" id="contentTable"></table>
+				<table:page page="${page}"></table:page>
+				<div style="clear:both;"></div>
+			</div>
+		</div>
+	</div>
+</div>
+<script src="${ctxStatic}/layer-v2.3/layui/layui.all.js" charset="utf-8"></script>
+<script>
+    var rankingType = $('#columnId').val() || '0';
+
+    layui.use('table', function(){
+        var cols;
+        if (rankingType === '2') {
+            cols = [[
+                {field:'index', align:'center', title: '排名', width:60}
+                ,{field:'userName', align: 'center', title: '姓名', minWidth: 120, templet: function(d) {
+                    var name = d.userName || '';
+                    return '<a href="javascript:void(0)" class="attention-info" onclick="openUserDetail(\'' + d.userId + '\',\'' + name.replace(/'/g, "\\'") + '\')">' + name + '</a>';
+                }}
+                ,{field:'officeName', align: 'center', title: '所属部门', minWidth: 160}
+                ,{field:'mobile', align: 'center', title: '手机号', minWidth: 130, templet: function(d) { return d.mobile || d.phone || ''; }}
+                ,{field:'exchangedContributionPoint', align: 'center', title: '贡献已兑换', width: 110}
+                ,{field:'remainingContributionPoint', align: 'center', title: '贡献剩余', width: 100}
+                ,{field:'contributionPoint', align: 'center', title: '贡献积分(总获取)', width: 130, templet: function(d) {
+                    return '<span style="color:#1565c0;font-weight:bold;">' + (d.contributionPoint || 0) + '</span>';
+                }}
+            ]];
+        } else if (rankingType === '1') {
+            cols = [[
+                {field:'index', align:'center', title: '排名', width:60}
+                ,{field:'userName', align: 'center', title: '姓名', minWidth: 120, templet: function(d) {
+                    var name = d.userName || '';
+                    return '<a href="javascript:void(0)" class="attention-info" onclick="openUserDetail(\'' + d.userId + '\',\'' + name.replace(/'/g, "\\'") + '\')">' + name + '</a>';
+                }}
+                ,{field:'officeName', align: 'center', title: '所属部门', minWidth: 160}
+                ,{field:'mobile', align: 'center', title: '手机号', minWidth: 130, templet: function(d) { return d.mobile || d.phone || ''; }}
+                ,{field:'exchangedBasicPoint', align: 'center', title: '基础已兑换', width: 110}
+                ,{field:'remainingBasicPoint', align: 'center', title: '基础剩余', width: 100}
+                ,{field:'basicPoint', align: 'center', title: '基础积分(总获取)', width: 130, templet: function(d) {
+                    return '<span style="color:#2e7d32;font-weight:bold;">' + (d.basicPoint || 0) + '</span>';
+                }}
+            ]];
+        } else {
+            cols = [[
+                {field:'index', align:'center', title: '排名', width:60}
+                ,{field:'userName', align: 'center', title: '姓名', minWidth: 120, templet: function(d) {
+                    var name = d.userName || '';
+                    return '<a href="javascript:void(0)" class="attention-info" onclick="openUserDetail(\'' + d.userId + '\',\'' + name.replace(/'/g, "\\'") + '\')">' + name + '</a>';
+                }}
+                ,{field:'officeName', align: 'center', title: '所属部门', minWidth: 160}
+                ,{field:'mobile', align: 'center', title: '手机号', minWidth: 130, templet: function(d) { return d.mobile || d.phone || ''; }}
+                ,{field:'basicPoint', align: 'center', title: '基础积分(总获取)', width: 120, templet: function(d) {
+                    return '<span style="color:#2e7d32;font-weight:bold;">' + (d.basicPoint || 0) + '</span>';
+                }}
+                ,{field:'exchangedBasicPoint', align: 'center', title: '基础已兑换', width: 100}
+                ,{field:'remainingBasicPoint', align: 'center', title: '基础剩余', width: 90}
+                ,{field:'contributionPoint', align: 'center', title: '贡献积分(总获取)', width: 130, templet: function(d) {
+                    return '<span style="color:#1565c0;font-weight:bold;">' + (d.contributionPoint || 0) + '</span>';
+                }}
+                ,{field:'exchangedContributionPoint', align: 'center', title: '贡献已兑换', width: 110}
+                ,{field:'remainingContributionPoint', align: 'center', title: '贡献剩余', width: 100}
+                ,{field:'totalPoint', align: 'center', title: '总积分', width: 100, templet: function(d) {
+                    return '<span style="color:#1aa094;font-weight:bold;font-size:15px;">' + (d.totalPoint || 0) + '</span>';
+                }}
+            ]];
+        }
+
+        layui.table.render({
+            limit:${ page.pageSize }
+            ,elem: '#contentTable'
+            ,page: false
+            ,cols: cols
+            ,data: [
+                <c:choose>
+                    <c:when test="${not empty page.list}">
+                        <c:forEach items="${page.list}" var="row" varStatus="st">
+                            <c:if test="${st.index != 0}">,</c:if>
+                            {
+                                "index": "${st.index + 1 + (page.pageNo - 1) * page.pageSize}"
+                                ,"userId": "${row.userId}"
+                                ,"userName": "<c:out value='${row.userName}'/>"
+                                ,"officeName": "<c:out value='${row.officeName}'/>"
+                                ,"mobile": "<c:out value='${row.mobile}'/>"
+                                ,"phone": "<c:out value='${row.phone}'/>"
+                                ,"basicPoint": "${row.basicPoint}"
+                                ,"exchangedBasicPoint": "${row.exchangedBasicPoint}"
+                                ,"remainingBasicPoint": "${row.remainingBasicPoint}"
+                                ,"contributionPoint": "${row.contributionPoint}"
+                                ,"exchangedContributionPoint": "${row.exchangedContributionPoint}"
+                                ,"remainingContributionPoint": "${row.remainingContributionPoint}"
+                                ,"totalPoint": "${row.totalPoint}"
+                            }
+                        </c:forEach>
+                    </c:when>
+                    <c:otherwise></c:otherwise>
+                </c:choose>
+            ]
+        });
+    });
+
+    function exportRanking() {
+        var type = $('#columnId').val() || '0';
+        var submitterId = $('input[name="submitterId"]').val() || '';
+        var officeId = $('input[name="officeId"]').val() || '';
+        var gainBeginDate = $('#gainBeginDate').val() || '';
+        var gainEndDate = $('#gainEndDate').val() || '';
+        var randomId = "down_" + new Date().getTime();
+        var downForm = $('<form>', {
+            'action': '${ctx}/workKnowledgeBase/point/exportRanking',
+            'method': 'GET',
+            'style': 'display:none;'
+        }).appendTo('body');
+        $('<input>').attr({ type: 'hidden', name: 'rankingType', value: type }).appendTo(downForm);
+        if (submitterId) $('<input>').attr({ type: 'hidden', name: 'submitterId', value: submitterId }).appendTo(downForm);
+        if (officeId) $('<input>').attr({ type: 'hidden', name: 'officeId', value: officeId }).appendTo(downForm);
+        if (gainBeginDate) $('<input>').attr({ type: 'hidden', name: 'gainBeginDate', value: gainBeginDate }).appendTo(downForm);
+        if (gainEndDate) $('<input>').attr({ type: 'hidden', name: 'gainEndDate', value: gainEndDate }).appendTo(downForm);
+        var downIframe = $('<iframe>', { 'name': randomId, 'style': 'display:none;' }).appendTo('body');
+        downForm.attr('target', randomId);
+        downIframe.on('load', function() {
+            try { downForm.remove(); downIframe.remove(); } catch(e) {}
+        });
+        downForm.submit();
+        setTimeout(function() {
+            try { downForm.remove(); downIframe.remove(); } catch(e) {}
+        }, 3000);
+    }
+</script>
+</body>
+</html>

+ 4 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseReadDetail.jsp

@@ -11,6 +11,10 @@
     </script>
 </head>
 <body>
+<form:form id="searchForm" modelAttribute="page" action="${ctx}/workKnowledgeBase/readLike/readDetailPage?fileId=${fileId}" method="post" style="display:none;">
+    <input type="hidden" name="pageNo" id="pageNo" value="${page.pageNo}"/>
+    <input type="hidden" name="pageSize" id="pageSize" value="${page.pageSize}"/>
+</form:form>
 <div class="wrapper wrapper-content">
     <div class="layui-row">
         <div class="full-width fl">

+ 31 - 9
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareList.jsp

@@ -730,16 +730,38 @@
             });
         }
 
-        /** 显示点击详情弹窗 */
+        /** 显示点击详情弹窗(先记录点击量,再打开详情) */
         function showClickDetail(fileId) {
-            top.layer.open({
-                type: 2,
-                area: ['80%', '80%'],
-                title: '点击详情',
-                maxmin: true,
-                content: '${ctx}/workKnowledgeBase/readLike/clickDetailPage?fileId=' + fileId,
-                btn: ['关闭'],
-                btn1: function(index) { top.layer.close(index); }
+            $.ajax({
+                type: 'POST',
+                url: '${ctx}/workKnowledgeBase/readLike/incrementClick',
+                data: { fileId: fileId },
+                success: function(data) {
+                    if (data.success) {
+                        updateCountInList(fileId, 'click-count', data.clickCount);
+                    }
+                    top.layer.open({
+                        type: 2,
+                        area: ['80%', '80%'],
+                        title: '点击详情',
+                        maxmin: true,
+                        content: '${ctx}/workKnowledgeBase/readLike/clickDetailPage?fileId=' + fileId,
+                        btn: ['关闭'],
+                        btn1: function(index) { top.layer.close(index); }
+                    });
+                },
+                error: function() {
+                    // 即使记录失败也打开详情
+                    top.layer.open({
+                        type: 2,
+                        area: ['80%', '80%'],
+                        title: '点击详情',
+                        maxmin: true,
+                        content: '${ctx}/workKnowledgeBase/readLike/clickDetailPage?fileId=' + fileId,
+                        btn: ['关闭'],
+                        btn1: function(index) { top.layer.close(index); }
+                    });
+                }
             });
         }