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

知识库,通知栏月报统计功能

徐滕 1 hete
szülő
commit
cb16da452b
33 módosított fájl, 3736 hozzáadás és 62 törlés
  1. 196 0
      docs/知识库积分统计看板开发说明.md
  2. 44 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WkPointSnapshotDetailDao.java
  3. 46 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WkPointSnapshotInfoDao.java
  4. 46 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WkPointSnapshotSummaryDao.java
  5. 12 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBasePointDetailDao.java
  6. 21 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgePointNotifyUserDao.java
  7. 22 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/UserPointDetail.java
  8. 115 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WkPointSnapshotDetail.java
  9. 144 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WkPointSnapshotInfo.java
  10. 95 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WkPointSnapshotSummary.java
  11. 29 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgePointNotifyUser.java
  12. 616 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WkPointSnapshotService.java
  13. 13 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBasePointDetailService.java
  14. 1 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java
  15. 14 3
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgePointCategoryService.java
  16. 361 40
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBasePointController.java
  17. 34 0
      src/main/java/com/jeeplus/modules/ruralprojectrecords/web/RuralProjectSignatureOldMessageDisposeController.java
  18. 30 0
      src/main/java/com/jeeplus/modules/workcalendar/service/WorkCalendarTaskService.java
  19. 19 0
      src/main/java/com/jeeplus/modules/workprojectnotify/web/WorkProjectNotifyController.java
  20. 246 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WkPointSnapshotDetailDao.xml
  21. 159 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WkPointSnapshotInfoDao.xml
  22. 151 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WkPointSnapshotSummaryDao.xml
  23. 14 1
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDao.xml
  24. 129 3
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml
  25. 42 8
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseShareInfoDao.xml
  26. 1 1
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgePointCategoryDao.xml
  27. 49 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgePointNotifyUserDao.xml
  28. 104 0
      src/main/resources/mysqlDateBase/wk_point_snapshot_tables.sql
  29. 407 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointDashboard.jsp
  30. 291 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointDeptEmployees.jsp
  31. 279 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointDrillDown.jsp
  32. 2 2
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointList.jsp
  33. 4 4
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareList.jsp

+ 196 - 0
docs/知识库积分统计看板开发说明.md

@@ -0,0 +1,196 @@
+# 知识库积分统计看板 - 开发说明
+
+## 一、功能概述
+
+每月1号凌晨5点自动生成上个月的积分快照数据,并向指定人员发送通知。
+通知点击后展示积分统计看板,支持公司级/部门级/员工级三层下钻查看。
+
+### 入口
+1. **通知入口**:点击待办通知(type=201)直接进入看板页面
+2. **菜单入口**:知识库管理 → 积分看板(支持手动选择月份查看)
+
+### 数据层级
+| 层级 | 说明 | 交互 |
+|------|------|------|
+| 公司级 | 全公司汇总卡片 | 默认展示 |
+| 部门级 | 各部门汇总表格 | 点击部门名称弹出员工列表 |
+| 员工级 | 部门下员工明细 | 点击"详情"弹出积分明细 |
+
+---
+
+## 二、数据库表
+
+### 2.1 知识库积分通知人员表
+
+```sql
+CREATE TABLE `work_knowledge_point_notify_user` (
+    `id` VARCHAR(64) NOT NULL COMMENT '主键',
+    `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID(关联sys_user.id)',
+    `del_flag` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标记(0正常 1删除)',
+    `create_date` DATETIME DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库积分通知人员表';
+```
+
+**使用说明**:在表中插入需要接收通知的用户记录即可。
+```sql
+-- 示例:添加通知人员
+INSERT INTO work_knowledge_point_notify_user (id, user_id, del_flag, create_date)
+VALUES (REPLACE(UUID(), '-', ''), '用户ID', '0', NOW());
+```
+
+### 2.2 知识库积分快照明细表(员工级)
+
+```sql
+CREATE TABLE `wk_point_snapshot_info` (
+    `id` VARCHAR(64) NOT NULL COMMENT '主键',
+    `del_flag` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标记',
+    `create_date` DATETIME DEFAULT NULL COMMENT '创建时间',
+    `snapshot_year` INT NOT NULL COMMENT '快照年份',
+    `snapshot_month` INT NOT NULL COMMENT '快照月份',
+    `office_id` VARCHAR(64) DEFAULT NULL COMMENT '部门ID',
+    `office_name` VARCHAR(200) DEFAULT NULL COMMENT '部门名称',
+    `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
+    `user_name` VARCHAR(200) DEFAULT NULL COMMENT '用户姓名',
+    `upload_count` INT DEFAULT 0 COMMENT '上传文档数',
+    `field1` VARCHAR(64) DEFAULT NULL COMMENT '基础-阅读积分',
+    `field2` VARCHAR(64) DEFAULT NULL COMMENT '基础-点赞积分',
+    `field3` VARCHAR(64) DEFAULT NULL COMMENT '贡献-技术总结-创建',
+    `field4` VARCHAR(64) DEFAULT NULL COMMENT '贡献-技术总结-审核',
+    `field5` VARCHAR(64) DEFAULT NULL COMMENT '贡献-培训心得-创建',
+    `field6` VARCHAR(64) DEFAULT NULL COMMENT '贡献-培训心得-审核',
+    `field7` VARCHAR(64) DEFAULT NULL COMMENT '贡献-相关资料-创建',
+    `field8` VARCHAR(64) DEFAULT NULL COMMENT '贡献-相关资料-审核',
+    `field9` VARCHAR(64) DEFAULT NULL COMMENT '贡献-问答答疑-创建',
+    `field10` VARCHAR(64) DEFAULT NULL COMMENT '贡献-问答答疑-回答',
+    `field11` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(原创)-创建',
+    `field12` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(原创)-审核',
+    `field13` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(转载)-创建',
+    `field14` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(转载)-审核',
+    `field15` VARCHAR(64) DEFAULT NULL COMMENT '贡献-扣除积分',
+    `field16` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段16',
+    `field17` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段17',
+    `field18` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段18',
+    `field19` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段19',
+    `field20` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段20',
+    PRIMARY KEY (`id`),
+    KEY `idx_snapshot_ym` (`snapshot_year`, `snapshot_month`),
+    KEY `idx_snapshot_user` (`snapshot_year`, `snapshot_month`, `user_id`),
+    KEY `idx_snapshot_office` (`snapshot_year`, `snapshot_month`, `office_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库积分快照明细表';
+```
+
+### 2.3 知识库积分快照汇总表(公司/部门/员工三级)
+
+```sql
+CREATE TABLE `wk_point_snapshot_summary` (
+    `id` VARCHAR(64) NOT NULL COMMENT '主键',
+    `del_flag` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标记',
+    `create_date` DATETIME DEFAULT NULL COMMENT '创建时间',
+    `snapshot_year` INT NOT NULL COMMENT '快照年份',
+    `snapshot_month` INT NOT NULL COMMENT '快照月份',
+    `grade_type` VARCHAR(10) NOT NULL COMMENT '层级类型(1:公司 2:部门 3:员工)',
+    `office_id` VARCHAR(64) DEFAULT NULL COMMENT '部门ID(公司级为空)',
+    `office_name` VARCHAR(200) DEFAULT NULL COMMENT '部门名称',
+    `user_id` VARCHAR(64) DEFAULT NULL COMMENT '用户ID(员工级才有)',
+    `user_name` VARCHAR(200) DEFAULT NULL COMMENT '用户姓名',
+    `upload_count` INT DEFAULT 0 COMMENT '上传文档数',
+    `basic_subtotal` INT DEFAULT 0 COMMENT '基础积分汇总',
+    `basic_exchange` INT DEFAULT 0 COMMENT '基础已兑换积分',
+    `basic_remaining` INT DEFAULT 0 COMMENT '基础剩余积分',
+    `contrib_subtotal` INT DEFAULT 0 COMMENT '贡献积分汇总(含扣除)',
+    `contrib_deduction` INT DEFAULT 0 COMMENT '贡献-扣除积分',
+    `contrib_exchange` INT DEFAULT 0 COMMENT '贡献已兑换积分',
+    `contrib_remaining` INT DEFAULT 0 COMMENT '贡献剩余积分',
+    PRIMARY KEY (`id`),
+    KEY `idx_summary_ym` (`snapshot_year`, `snapshot_month`),
+    KEY `idx_summary_ym_grade` (`snapshot_year`, `snapshot_month`, `grade_type`),
+    KEY `idx_summary_ym_office` (`snapshot_year`, `snapshot_month`, `office_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库积分快照汇总表';
+```
+
+---
+
+## 三、通知参数说明
+
+| 参数 | 值 | 说明 |
+|------|------|------|
+| utilNotifyId | `201` | 通知类型标识 |
+| notifyId | `201,{year},{month}` | 如 `201,2026,6` |
+| type | `201` | WorkProjectNotify.type |
+| title | `{year}年{month}月-知识库积分统计` | 通知标题 |
+| content | `{year}年{month}月-知识库积分统计` | 通知内容 |
+| status | `0` | 待办状态 |
+| remarks | `待通知` | 备注 |
+
+---
+
+## 四、定时任务
+
+| 项目 | 说明 |
+|------|------|
+| 类 | `WorkCalendarTaskService` |
+| 方法 | `getKnowledgeBasePointSnapshotOnMonth()` |
+| cron | `0 0 5 1 * ?`(每月1号凌晨5点) |
+| 逻辑 | 计算上个月的年月 → 生成快照 → 发送通知 |
+
+---
+
+## 五、接口清单
+
+| 接口 | URL | 方法 | 说明 |
+|------|------|------|------|
+| 看板页面 | `/workKnowledgeBase/point/dashboard` | GET | 从菜单进入的看板页面 |
+| 看板数据 | `/workKnowledgeBase/point/dashboardData` | GET | AJAX,参数:year, month |
+| 部门员工 | `/workKnowledgeBase/point/departmentEmployees` | GET | AJAX,参数:year, month, officeId |
+| 员工详情 | `/workKnowledgeBase/point/employeeSnapshot` | GET | AJAX,参数:year, month, userId |
+| 手动生成 | `/workKnowledgeBase/point/generateSnapshot` | GET | AJAX,参数:year, month(需listAll权限) |
+
+---
+
+## 六、文件清单
+
+### 新增文件
+| 文件 | 说明 |
+|------|------|
+| `entity/WkPointSnapshotInfo.java` | 快照明细实体类 |
+| `entity/WkPointSnapshotSummary.java` | 快照汇总实体类 |
+| `entity/WorkKnowledgePointNotifyUser.java` | 通知人员实体类 |
+| `dao/WkPointSnapshotInfoDao.java` | 快照明细DAO |
+| `dao/WkPointSnapshotSummaryDao.java` | 快照汇总DAO |
+| `dao/WorkKnowledgePointNotifyUserDao.java` | 通知人员DAO |
+| `service/WkPointSnapshotService.java` | 快照Service(核心计算+通知发送) |
+| `WkPointSnapshotInfoDao.xml` | 快照明细MyBatis映射 |
+| `WkPointSnapshotSummaryDao.xml` | 快照汇总MyBatis映射 |
+| `WorkKnowledgePointNotifyUserDao.xml` | 通知人员MyBatis映射 |
+| `workKnowledgeBasePointDashboard.jsp` | 看板展示页面 |
+
+### 修改文件
+| 文件 | 修改内容 |
+|------|------|
+| `WorkCalendarTaskService.java` | 新增定时任务方法 + 注入WkPointSnapshotService |
+| `WorkProjectNotifyController.java` | 新增type=201的通知处理分支 + 注入WkPointSnapshotService |
+| `WorkKnowledgeBasePointController.java` | 新增看板页面入口和5个AJAX接口 + 注入WkPointSnapshotService |
+
+---
+
+## 七、部署步骤
+
+1. **执行建表SQL**:按第二节依次执行3张表的CREATE TABLE语句
+2. **插入通知人员**:在 `work_knowledge_point_notify_user` 表中添加需要接收通知的用户
+3. **部署代码**:编译打包部署
+4. **手动测试**:调用 `/workKnowledgeBase/point/generateSnapshot?year=2026&month=6` 生成测试数据
+5. **查看看板**:访问 `/workKnowledgeBase/point/dashboard` 查看效果
+6. **验证通知**:等待每月1号凌晨5点自动执行,或手动触发
+
+---
+
+## 八、扩展字段说明
+
+`wk_point_snapshot_info` 表预留了 `field16` ~ `field20` 共5个扩展字段。
+如需新增规则积分列:
+1. 在 `WkPointSnapshotService.FIELD_MAPPING` 中添加映射
+2. 在 `buildRuleIdToFieldMapping()` 中添加规则匹配
+3. 在 `setField()` 方法中添加 case 分支
+4. 在 JSP 详情弹窗中添加对应行

+ 44 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WkPointSnapshotDetailDao.java

@@ -0,0 +1,44 @@
+package com.jeeplus.modules.WorkKnowledgeBase.dao;
+
+import com.jeeplus.common.persistence.CrudDao;
+import com.jeeplus.common.persistence.annotation.MyBatisDao;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotDetail;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 知识库积分快照明细表 DAO
+ * @author system
+ * @version 2026-07-06
+ */
+@MyBatisDao
+public interface WkPointSnapshotDetailDao extends CrudDao<WkPointSnapshotDetail> {
+
+    /**
+     * 从 point_detail 表查询指定日期范围内的所有积分明细(用于快照生成)
+     */
+    List<WkPointSnapshotDetail> findAllPointDetails(@Param("beginDate") String beginDate,
+                                                     @Param("endDate") String endDate);
+
+    /**
+     * 按钻取条件查询快照明细(分页列表)
+     */
+    List<WkPointSnapshotDetail> findDrillDownList(WkPointSnapshotDetail query);
+
+    /**
+     * 按钻取条件查询快照明细总数
+     */
+    int findDrillDownCount(WkPointSnapshotDetail query);
+
+    /**
+     * 批量插入快照明细
+     */
+    void batchInsert(@Param("list") List<WkPointSnapshotDetail> list);
+
+    /**
+     * 删除指定年月的快照明细
+     */
+    void deleteByYearMonth(@Param("snapshotYear") Integer snapshotYear,
+                           @Param("snapshotMonth") Integer snapshotMonth);
+}

+ 46 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WkPointSnapshotInfoDao.java

@@ -0,0 +1,46 @@
+package com.jeeplus.modules.WorkKnowledgeBase.dao;
+
+import com.jeeplus.common.persistence.CrudDao;
+import com.jeeplus.common.persistence.annotation.MyBatisDao;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 知识库积分快照明细表 DAO
+ * @author system
+ * @version 2026-07-06
+ */
+@MyBatisDao
+public interface WkPointSnapshotInfoDao extends CrudDao<WkPointSnapshotInfo> {
+
+    /**
+     * 按年月查询快照明细(员工级)
+     */
+    List<WkPointSnapshotInfo> findByYearMonth(@Param("snapshotYear") Integer snapshotYear,
+                                               @Param("snapshotMonth") Integer snapshotMonth);
+
+    /**
+     * 按年月+部门查询快照明细
+     */
+    List<WkPointSnapshotInfo> findByYearMonthAndOffice(@Param("snapshotYear") Integer snapshotYear,
+                                                        @Param("snapshotMonth") Integer snapshotMonth,
+                                                        @Param("officeId") String officeId,
+                                                        @Param("userId") String userId,
+                                                        @Param("userName") String userName,
+                                                        @Param("officeName") String officeName);
+
+    /**
+     * 按年月+用户查询快照明细
+     */
+    WkPointSnapshotInfo findByYearMonthAndUser(@Param("snapshotYear") Integer snapshotYear,
+                                                @Param("snapshotMonth") Integer snapshotMonth,
+                                                @Param("userId") String userId);
+
+    /**
+     * 删除指定年月的快照数据
+     */
+    void deleteByYearMonth(@Param("snapshotYear") Integer snapshotYear,
+                           @Param("snapshotMonth") Integer snapshotMonth);
+}

+ 46 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WkPointSnapshotSummaryDao.java

@@ -0,0 +1,46 @@
+package com.jeeplus.modules.WorkKnowledgeBase.dao;
+
+import com.jeeplus.common.persistence.CrudDao;
+import com.jeeplus.common.persistence.annotation.MyBatisDao;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 知识库积分快照汇总表 DAO
+ * @author system
+ * @version 2026-07-06
+ */
+@MyBatisDao
+public interface WkPointSnapshotSummaryDao extends CrudDao<WkPointSnapshotSummary> {
+
+    /**
+     * 按年月+层级查询快照汇总
+     */
+    List<WkPointSnapshotSummary> findByYearMonthAndGrade(@Param("snapshotYear") Integer snapshotYear,
+                                                          @Param("snapshotMonth") Integer snapshotMonth,
+                                                          @Param("gradeType") String gradeType);
+
+    /**
+     * 按年月查询所有层级快照汇总
+     */
+    List<WkPointSnapshotSummary> findByYearMonth(@Param("snapshotYear") Integer snapshotYear,
+                                                  @Param("snapshotMonth") Integer snapshotMonth);
+
+    /**
+     * 按年月+部门查询员工级快照
+     */
+    List<WkPointSnapshotSummary> findEmployeeByYearMonthAndOffice(@Param("snapshotYear") Integer snapshotYear,
+                                                                    @Param("snapshotMonth") Integer snapshotMonth,
+                                                                    @Param("officeId") String officeId,
+                                                                    @Param("userId") String userId,
+                                                                    @Param("userName") String userName,
+                                                                    @Param("officeName") String officeName);
+
+    /**
+     * 删除指定年月的快照汇总数据
+     */
+    void deleteByYearMonth(@Param("snapshotYear") Integer snapshotYear,
+                           @Param("snapshotMonth") Integer snapshotMonth);
+}

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

@@ -198,4 +198,16 @@ public interface WorkKnowledgeBasePointDetailDao extends CrudDao<WorkKnowledgeBa
         @Param("beginDate") String beginDate,
         @Param("endDate") String endDate
     );
+
+    /**
+     * 钻取查询:按部门+L2分类+规则类型查询积分明细文件列表
+     * @param userPointDetail 查询参数(officeId/categoryId/ruleType/beginDate/endDate)
+     * @return 积分明细列表
+     */
+    List<UserPointDetail> findDrillDownList(UserPointDetail userPointDetail);
+
+    /**
+     * 钻取查询总数
+     */
+    int findDrillDownCount(UserPointDetail userPointDetail);
 }

+ 21 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgePointNotifyUserDao.java

@@ -0,0 +1,21 @@
+package com.jeeplus.modules.WorkKnowledgeBase.dao;
+
+import com.jeeplus.common.persistence.CrudDao;
+import com.jeeplus.common.persistence.annotation.MyBatisDao;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgePointNotifyUser;
+
+import java.util.List;
+
+/**
+ * 知识库积分通知人员表 DAO
+ * @author system
+ * @version 2026-07-06
+ */
+@MyBatisDao
+public interface WorkKnowledgePointNotifyUserDao extends CrudDao<WorkKnowledgePointNotifyUser> {
+
+    /**
+     * 查询所有通知人员
+     */
+    List<WorkKnowledgePointNotifyUser> findAllNotifyUsers();
+}

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

@@ -57,6 +57,12 @@ public class UserPointDetail extends DataEntity<UserPointDetail> {
     /** 积分类型筛选:acquire-积分获取,exchange-积分兑换 */
     private String pointType;
 
+    /** 部门ID(钻取查询用,空字符串=公司级) */
+    private String officeId;
+
+    /** 部门名称(钻取查询结果用) */
+    private String officeName;
+
     public String getId() {
         return id;
     }
@@ -192,4 +198,20 @@ public class UserPointDetail extends DataEntity<UserPointDetail> {
     public void setPointType(String pointType) {
         this.pointType = pointType;
     }
+
+    public String getOfficeId() {
+        return officeId;
+    }
+
+    public void setOfficeId(String officeId) {
+        this.officeId = officeId;
+    }
+
+    public String getOfficeName() {
+        return officeName;
+    }
+
+    public void setOfficeName(String officeName) {
+        this.officeName = officeName;
+    }
 }

+ 115 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WkPointSnapshotDetail.java

@@ -0,0 +1,115 @@
+package com.jeeplus.modules.WorkKnowledgeBase.entity;
+
+import com.jeeplus.common.persistence.DataEntity;
+
+import java.util.Date;
+
+/**
+ * 知识库积分快照明细表 Entity
+ * 保存每条积分明细的快照,用于钻取查询,确保与看板数字一致
+ * @author system
+ * @version 2026-07-06
+ */
+public class WkPointSnapshotDetail extends DataEntity<WkPointSnapshotDetail> {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer snapshotYear;       // 快照年份
+    private Integer snapshotMonth;      // 快照月份
+    private String officeId;            // 部门ID
+    private String officeName;          // 部门名称
+    private String userId;              // 用户ID
+    private String userName;            // 用户姓名
+    private String shareId;             // 文件ID
+    private String shareName;           // 文件名称
+    private String treeNodeId;          // 树节点ID
+    private String shareDelFlag;        // 文件删除标志
+    private String categoryId;          // 分类ID
+    private String categoryName;        // 分类名称
+    private String ruleId;              // 规则ID
+    private Integer ruleType;           // 规则类型(1=创建,2=审核,10=阅读,20=点赞)
+    private Integer point;              // 积分值
+    private Date pointCreateDate;       // 积分产生时间
+
+    // 查询条件(非持久化)
+    private String columnId;            // 钻取用:deduction=扣除积分
+    private String beginDate;
+    private String endDate;
+    private String shareNameQuery;      // 文档名称模糊查询
+    private String userNameQuery;       // 人员姓名模糊查询
+    private String officeNameQuery;     // 部门名称模糊查询
+
+    public WkPointSnapshotDetail() {
+        super();
+    }
+
+    public WkPointSnapshotDetail(String id) {
+        super(id);
+    }
+
+    public Integer getSnapshotYear() { return snapshotYear; }
+    public void setSnapshotYear(Integer snapshotYear) { this.snapshotYear = snapshotYear; }
+
+    public Integer getSnapshotMonth() { return snapshotMonth; }
+    public void setSnapshotMonth(Integer snapshotMonth) { this.snapshotMonth = snapshotMonth; }
+
+    public String getOfficeId() { return officeId; }
+    public void setOfficeId(String officeId) { this.officeId = officeId; }
+
+    public String getOfficeName() { return officeName; }
+    public void setOfficeName(String officeName) { this.officeName = officeName; }
+
+    public String getUserId() { return userId; }
+    public void setUserId(String userId) { this.userId = userId; }
+
+    public String getUserName() { return userName; }
+    public void setUserName(String userName) { this.userName = userName; }
+
+    public String getShareId() { return shareId; }
+    public void setShareId(String shareId) { this.shareId = shareId; }
+
+    public String getShareName() { return shareName; }
+    public void setShareName(String shareName) { this.shareName = shareName; }
+
+    public String getTreeNodeId() { return treeNodeId; }
+    public void setTreeNodeId(String treeNodeId) { this.treeNodeId = treeNodeId; }
+
+    public String getShareDelFlag() { return shareDelFlag; }
+    public void setShareDelFlag(String shareDelFlag) { this.shareDelFlag = shareDelFlag; }
+
+    public String getCategoryId() { return categoryId; }
+    public void setCategoryId(String categoryId) { this.categoryId = categoryId; }
+
+    public String getCategoryName() { return categoryName; }
+    public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
+
+    public String getRuleId() { return ruleId; }
+    public void setRuleId(String ruleId) { this.ruleId = ruleId; }
+
+    public Integer getRuleType() { return ruleType; }
+    public void setRuleType(Integer ruleType) { this.ruleType = ruleType; }
+
+    public Integer getPoint() { return point; }
+    public void setPoint(Integer point) { this.point = point; }
+
+    public Date getPointCreateDate() { return pointCreateDate; }
+    public void setPointCreateDate(Date pointCreateDate) { this.pointCreateDate = pointCreateDate; }
+
+    public String getColumnId() { return columnId; }
+    public void setColumnId(String columnId) { this.columnId = columnId; }
+
+    public String getBeginDate() { return beginDate; }
+    public void setBeginDate(String beginDate) { this.beginDate = beginDate; }
+
+    public String getEndDate() { return endDate; }
+    public void setEndDate(String endDate) { this.endDate = endDate; }
+
+    public String getShareNameQuery() { return shareNameQuery; }
+    public void setShareNameQuery(String shareNameQuery) { this.shareNameQuery = shareNameQuery; }
+
+    public String getUserNameQuery() { return userNameQuery; }
+    public void setUserNameQuery(String userNameQuery) { this.userNameQuery = userNameQuery; }
+
+    public String getOfficeNameQuery() { return officeNameQuery; }
+    public void setOfficeNameQuery(String officeNameQuery) { this.officeNameQuery = officeNameQuery; }
+}

+ 144 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WkPointSnapshotInfo.java

@@ -0,0 +1,144 @@
+/**
+ * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
+ */
+package com.jeeplus.modules.WorkKnowledgeBase.entity;
+
+import com.jeeplus.common.persistence.DataEntity;
+
+/**
+ * 知识库积分快照明细表(员工级)Entity
+ * field1~field15 存各规则积分值,field16~field20 为扩展字段
+ * @author system
+ * @version 2026-07-06
+ */
+public class WkPointSnapshotInfo extends DataEntity<WkPointSnapshotInfo> {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer snapshotYear;       // 年份
+    private Integer snapshotMonth;      // 月份
+    private String officeId;            // 部门ID
+    private String officeName;          // 部门名称
+    private String userId;              // 用户ID
+    private String userName;            // 用户姓名
+    private Integer uploadCount;        // 上传文档数
+    private String field1;              // 基础-阅读积分
+    private String field2;              // 基础-点赞积分
+    private String field3;              // 贡献-技术总结-创建
+    private String field4;              // 贡献-技术总结-审核
+    private String field5;              // 贡献-培训心得-创建
+    private String field6;              // 贡献-培训心得-审核
+    private String field7;              // 贡献-相关资料-创建
+    private String field8;              // 贡献-相关资料-审核
+    private String field9;              // 贡献-问答答疑-创建
+    private String field10;             // 贡献-问答答疑-回答
+    private String field11;             // 贡献-典型案例(原创)-创建
+    private String field12;             // 贡献-典型案例(原创)-审核
+    private String field13;             // 贡献-典型案例(转载)-创建
+    private String field14;             // 贡献-典型案例(转载)-审核
+    private String field15;             // 贡献-扣除积分
+    private String field16;             // 扩展字段16
+    private String field17;             // 扩展字段17
+    private String field18;             // 扩展字段18
+    private String field19;             // 扩展字段19
+    private String field20;             // 扩展字段20
+
+    // 查询条件
+    private String beginDate;
+    private String endDate;
+
+    public WkPointSnapshotInfo() {
+        super();
+    }
+
+    public WkPointSnapshotInfo(String id) {
+        super(id);
+    }
+
+    public Integer getSnapshotYear() { return snapshotYear; }
+    public void setSnapshotYear(Integer snapshotYear) { this.snapshotYear = snapshotYear; }
+
+    public Integer getSnapshotMonth() { return snapshotMonth; }
+    public void setSnapshotMonth(Integer snapshotMonth) { this.snapshotMonth = snapshotMonth; }
+
+    public String getOfficeId() { return officeId; }
+    public void setOfficeId(String officeId) { this.officeId = officeId; }
+
+    public String getOfficeName() { return officeName; }
+    public void setOfficeName(String officeName) { this.officeName = officeName; }
+
+    public String getUserId() { return userId; }
+    public void setUserId(String userId) { this.userId = userId; }
+
+    public String getUserName() { return userName; }
+    public void setUserName(String userName) { this.userName = userName; }
+
+    public Integer getUploadCount() { return uploadCount; }
+    public void setUploadCount(Integer uploadCount) { this.uploadCount = uploadCount; }
+
+    public String getField1() { return field1; }
+    public void setField1(String field1) { this.field1 = field1; }
+
+    public String getField2() { return field2; }
+    public void setField2(String field2) { this.field2 = field2; }
+
+    public String getField3() { return field3; }
+    public void setField3(String field3) { this.field3 = field3; }
+
+    public String getField4() { return field4; }
+    public void setField4(String field4) { this.field4 = field4; }
+
+    public String getField5() { return field5; }
+    public void setField5(String field5) { this.field5 = field5; }
+
+    public String getField6() { return field6; }
+    public void setField6(String field6) { this.field6 = field6; }
+
+    public String getField7() { return field7; }
+    public void setField7(String field7) { this.field7 = field7; }
+
+    public String getField8() { return field8; }
+    public void setField8(String field8) { this.field8 = field8; }
+
+    public String getField9() { return field9; }
+    public void setField9(String field9) { this.field9 = field9; }
+
+    public String getField10() { return field10; }
+    public void setField10(String field10) { this.field10 = field10; }
+
+    public String getField11() { return field11; }
+    public void setField11(String field11) { this.field11 = field11; }
+
+    public String getField12() { return field12; }
+    public void setField12(String field12) { this.field12 = field12; }
+
+    public String getField13() { return field13; }
+    public void setField13(String field13) { this.field13 = field13; }
+
+    public String getField14() { return field14; }
+    public void setField14(String field14) { this.field14 = field14; }
+
+    public String getField15() { return field15; }
+    public void setField15(String field15) { this.field15 = field15; }
+
+    public String getField16() { return field16; }
+    public void setField16(String field16) { this.field16 = field16; }
+
+    public String getField17() { return field17; }
+    public void setField17(String field17) { this.field17 = field17; }
+
+    public String getField18() { return field18; }
+    public void setField18(String field18) { this.field18 = field18; }
+
+    public String getField19() { return field19; }
+    public void setField19(String field19) { this.field19 = field19; }
+
+    public String getField20() { return field20; }
+    public void setField20(String field20) { this.field20 = field20; }
+
+    public String getBeginDate() { return beginDate; }
+    public void setBeginDate(String beginDate) { this.beginDate = beginDate; }
+
+    public String getEndDate() { return endDate; }
+    public void setEndDate(String endDate) { this.endDate = endDate; }
+}

+ 95 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WkPointSnapshotSummary.java

@@ -0,0 +1,95 @@
+/**
+ * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
+ */
+package com.jeeplus.modules.WorkKnowledgeBase.entity;
+
+import com.jeeplus.common.persistence.DataEntity;
+
+/**
+ * 知识库积分快照汇总表(公司/部门/员工三级)Entity
+ * @author system
+ * @version 2026-07-06
+ */
+public class WkPointSnapshotSummary extends DataEntity<WkPointSnapshotSummary> {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer snapshotYear;       // 年份
+    private Integer snapshotMonth;      // 月份
+    private String gradeType;           // 层级类型(1:公司 2:部门 3:员工)
+    private String officeId;            // 部门ID(公司级为空)
+    private String officeName;          // 部门名称
+    private String userId;              // 用户ID(员工级才有)
+    private String userName;            // 用户姓名
+    private Integer uploadCount;        // 上传文档数
+    private Integer basicSubtotal;      // 基础积分汇总
+    private Integer basicExchange;      // 基础已兑换积分
+    private Integer basicRemaining;     // 基础剩余积分
+    private Integer contribSubtotal;    // 贡献积分汇总(含扣除)
+    private Integer contribDeduction;   // 贡献-扣除积分
+    private Integer contribExchange;    // 贡献已兑换积分
+    private Integer contribRemaining;   // 贡献剩余积分
+
+    // 查询条件
+    private String beginDate;
+    private String endDate;
+
+    public WkPointSnapshotSummary() {
+        super();
+    }
+
+    public WkPointSnapshotSummary(String id) {
+        super(id);
+    }
+
+    public Integer getSnapshotYear() { return snapshotYear; }
+    public void setSnapshotYear(Integer snapshotYear) { this.snapshotYear = snapshotYear; }
+
+    public Integer getSnapshotMonth() { return snapshotMonth; }
+    public void setSnapshotMonth(Integer snapshotMonth) { this.snapshotMonth = snapshotMonth; }
+
+    public String getGradeType() { return gradeType; }
+    public void setGradeType(String gradeType) { this.gradeType = gradeType; }
+
+    public String getOfficeId() { return officeId; }
+    public void setOfficeId(String officeId) { this.officeId = officeId; }
+
+    public String getOfficeName() { return officeName; }
+    public void setOfficeName(String officeName) { this.officeName = officeName; }
+
+    public String getUserId() { return userId; }
+    public void setUserId(String userId) { this.userId = userId; }
+
+    public String getUserName() { return userName; }
+    public void setUserName(String userName) { this.userName = userName; }
+
+    public Integer getUploadCount() { return uploadCount; }
+    public void setUploadCount(Integer uploadCount) { this.uploadCount = uploadCount; }
+
+    public Integer getBasicSubtotal() { return basicSubtotal; }
+    public void setBasicSubtotal(Integer basicSubtotal) { this.basicSubtotal = basicSubtotal; }
+
+    public Integer getBasicExchange() { return basicExchange; }
+    public void setBasicExchange(Integer basicExchange) { this.basicExchange = basicExchange; }
+
+    public Integer getBasicRemaining() { return basicRemaining; }
+    public void setBasicRemaining(Integer basicRemaining) { this.basicRemaining = basicRemaining; }
+
+    public Integer getContribSubtotal() { return contribSubtotal; }
+    public void setContribSubtotal(Integer contribSubtotal) { this.contribSubtotal = contribSubtotal; }
+
+    public Integer getContribDeduction() { return contribDeduction; }
+    public void setContribDeduction(Integer contribDeduction) { this.contribDeduction = contribDeduction; }
+
+    public Integer getContribExchange() { return contribExchange; }
+    public void setContribExchange(Integer contribExchange) { this.contribExchange = contribExchange; }
+
+    public Integer getContribRemaining() { return contribRemaining; }
+    public void setContribRemaining(Integer contribRemaining) { this.contribRemaining = contribRemaining; }
+
+    public String getBeginDate() { return beginDate; }
+    public void setBeginDate(String beginDate) { this.beginDate = beginDate; }
+
+    public String getEndDate() { return endDate; }
+    public void setEndDate(String endDate) { this.endDate = endDate; }
+}

+ 29 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgePointNotifyUser.java

@@ -0,0 +1,29 @@
+/**
+ * Copyright &copy; 2015-2020 <a href="http://www.jeeplus.org/">JeePlus</a> All rights reserved.
+ */
+package com.jeeplus.modules.WorkKnowledgeBase.entity;
+
+import com.jeeplus.common.persistence.DataEntity;
+
+/**
+ * 知识库积分通知人员表 Entity
+ * @author system
+ * @version 2026-07-06
+ */
+public class WorkKnowledgePointNotifyUser extends DataEntity<WorkKnowledgePointNotifyUser> {
+
+    private static final long serialVersionUID = 1L;
+
+    private String userId;  // 用户ID(关联sys_user.id)
+
+    public WorkKnowledgePointNotifyUser() {
+        super();
+    }
+
+    public WorkKnowledgePointNotifyUser(String id) {
+        super(id);
+    }
+
+    public String getUserId() { return userId; }
+    public void setUserId(String userId) { this.userId = userId; }
+}

+ 616 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WkPointSnapshotService.java

@@ -0,0 +1,616 @@
+package com.jeeplus.modules.WorkKnowledgeBase.service;
+
+import com.jeeplus.common.persistence.Page;
+import com.jeeplus.common.utils.IdGen;
+import com.jeeplus.common.utils.StringUtils;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WkPointSnapshotDetailDao;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WkPointSnapshotInfoDao;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WkPointSnapshotSummaryDao;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgeBasePointRuleDao;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgePointNotifyUserDao;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotDetail;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePointRule;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgePointNotifyUser;
+import com.jeeplus.modules.sys.dao.UserDao;
+import com.jeeplus.modules.sys.entity.Office;
+import com.jeeplus.modules.sys.entity.User;
+import com.jeeplus.modules.sys.service.OfficeService;
+import com.jeeplus.modules.sys.utils.UserUtils;
+import com.jeeplus.modules.workprojectnotify.service.WorkProjectNotifyService;
+import com.jeeplus.modules.workprojectnotify.util.UtilNotify;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 知识库积分快照 Service
+ * 负责:计算月度快照数据、保存到快照表、发送通知
+ * @author system
+ * @version 2026-07-06
+ */
+@Service
+@Transactional(readOnly = true)
+public class WkPointSnapshotService {
+
+    private Logger logger = LoggerFactory.getLogger(WkPointSnapshotService.class);
+
+    @Autowired
+    private WkPointSnapshotDetailDao snapshotDetailDao;
+
+    @Autowired
+    private WkPointSnapshotInfoDao snapshotInfoDao;
+
+    @Autowired
+    private WkPointSnapshotSummaryDao snapshotSummaryDao;
+
+    @Autowired
+    private WorkKnowledgeBasePointDetailService pointDetailService;
+
+    @Autowired
+    private WorkKnowledgeBasePointRuleDao pointRuleDao;
+
+    @Autowired
+    private WorkKnowledgePointCategoryService categoryService;
+
+    @Autowired
+    private OfficeService officeService;
+
+    @Autowired
+    private UserDao userDao;
+
+    @Autowired
+    private WorkKnowledgePointNotifyUserDao notifyUserDao;
+
+    @Autowired
+    private WorkProjectNotifyService workProjectNotifyService;
+
+    // field 映射定义:field编号 -> 查询描述(用于日志)
+    private static final Map<Integer, String> FIELD_MAPPING = new LinkedHashMap<>();
+    static {
+        FIELD_MAPPING.put(1, "基础-阅读");
+        FIELD_MAPPING.put(2, "基础-点赞");
+        FIELD_MAPPING.put(3, "贡献-技术总结-创建");
+        FIELD_MAPPING.put(4, "贡献-技术总结-审核");
+        FIELD_MAPPING.put(5, "贡献-培训心得-创建");
+        FIELD_MAPPING.put(6, "贡献-培训心得-审核");
+        FIELD_MAPPING.put(7, "贡献-相关资料-创建");
+        FIELD_MAPPING.put(8, "贡献-相关资料-审核");
+        FIELD_MAPPING.put(9, "贡献-问答答疑-创建");
+        FIELD_MAPPING.put(10, "贡献-问答答疑-回答");
+        FIELD_MAPPING.put(11, "贡献-典型案例(原创)-创建");
+        FIELD_MAPPING.put(12, "贡献-典型案例(原创)-审核");
+        FIELD_MAPPING.put(13, "贡献-典型案例(转载)-创建");
+        FIELD_MAPPING.put(14, "贡献-典型案例(转载)-审核");
+        FIELD_MAPPING.put(15, "贡献-扣除积分");
+    }
+
+    /**
+     * 生成指定年月的快照数据
+     * @param year 年份
+     * @param month 月份
+     */
+    @Transactional(readOnly = false)
+    public void generateSnapshot(Integer year, Integer month) {
+        logger.info("-----------知识库积分快照生成({}年{}月)开始------------------", year, month);
+
+        // 计算月份日期范围
+        String beginDate = String.format("%04d-%02d-01", year, month);
+        Calendar cal = Calendar.getInstance();
+        cal.set(year, month - 1, 1);
+        int maxDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
+        String endDate = String.format("%04d-%02d-%02d", year, month, maxDay);
+
+        try {
+            // 1. 删除该年月的旧快照数据(防止重复生成)
+            snapshotInfoDao.deleteByYearMonth(year, month);
+            snapshotSummaryDao.deleteByYearMonth(year, month);
+            snapshotDetailDao.deleteByYearMonth(year, month);
+
+            // 1b. 生成快照明细(从 point_detail 实时表查询当月所有积分明细,保存到快照明细表)
+            List<WkPointSnapshotDetail> allDetails = snapshotDetailDao.findAllPointDetails(beginDate, endDate);
+            if (allDetails != null && !allDetails.isEmpty()) {
+                // 设置快照年月
+                for (WkPointSnapshotDetail detail : allDetails) {
+                    detail.setId(IdGen.uuid());
+                    detail.setSnapshotYear(year);
+                    detail.setSnapshotMonth(month);
+                }
+                // 分批插入(每批500条)
+                int batchSize = 500;
+                for (int i = 0; i < allDetails.size(); i += batchSize) {
+                    int end = Math.min(i + batchSize, allDetails.size());
+                    snapshotDetailDao.batchInsert(allDetails.subList(i, end));
+                }
+                logger.info("快照明细写入完成,共{}条记录", allDetails.size());
+            }
+
+            // 2. 查询所有员工积分数据
+            WorkKnowledgeBasePoint queryParam = new WorkKnowledgeBasePoint();
+            queryParam.setBeginDate(beginDate);
+            queryParam.setEndDate(endDate);
+            List<WorkKnowledgeBasePoint> employeeList = pointDetailService.findAllEmployeePointStats(queryParam);
+
+            if (employeeList == null || employeeList.isEmpty()) {
+                logger.info("-----------无积分数据,跳过快照生成------------------");
+                return;
+            }
+
+            // 3. 收集用户ID
+            List<String> userIds = employeeList.stream()
+                    .map(WorkKnowledgeBasePoint::getUserId)
+                    .filter(StringUtils::isNotBlank)
+                    .collect(Collectors.toList());
+
+            // 4. 按rule分组积分
+            Map<String, Map<String, Integer>> pointsByRule = new HashMap<>();
+            Map<String, Integer> deductionByUser = new HashMap<>();
+            if (!userIds.isEmpty()) {
+                List<Map<String, Object>> ruleResults =
+                        pointDetailService.sumPointsByRuleGrouped(userIds, beginDate, endDate);
+                for (Map<String, Object> r : ruleResults) {
+                    String uid = (String) r.get("userId");
+                    String catId = String.valueOf(r.get("categoryId"));
+                    String rid = String.valueOf(r.get("ruleId"));
+                    int pts = ((Number) r.get("totalPoint")).intValue();
+                    if ("10".equals(catId)) {
+                        deductionByUser.merge(uid, pts, Integer::sum);
+                    } else {
+                        pointsByRule.computeIfAbsent(uid, k -> new HashMap<>()).put(rid, pts);
+                    }
+                }
+            }
+
+            // 5. 使用getThreeLevelHeaderStructure获取实际的分类结构(与导出完全一致)
+            Map<String, Object> headerStructure = categoryService.getThreeLevelHeaderStructure();
+            @SuppressWarnings("unchecked")
+            List<Map<String, String>> columnKeys = (List<Map<String, String>>) headerStructure.get("columnKeys");
+            @SuppressWarnings("unchecked")
+            List<List<String>> secondLevelHeaders = (List<List<String>>) headerStructure.get("secondLevelHeaders");
+            @SuppressWarnings("unchecked")
+            List<List<List<String>>> thirdLevelHeaders = (List<List<List<String>>>) headerStructure.get("thirdLevelHeaders");
+
+            // 从columnKeys重建各列的ruleId映射(与导出中rowBuilder的逻辑一致)
+            // 基础积分L1索引=0, 贡献积分L1索引=1
+            int keyIdx = 0;
+            Map<Integer, String> basicDirectRuleMap = new HashMap<>(); // j -> ruleId (基础积分的直接规则)
+            Map<Integer, String> basicL2LeafCatMap = new HashMap<>();  // j -> categoryId (基础积分的L2叶子)
+            Map<Integer, String> contribRuleMap = new LinkedHashMap<>();     // (i,j,k)编码 -> ruleId (贡献积分的规则)
+            Set<Integer> contribL2LeafSet = new HashSet<>();           // i*1000+j 编码 (贡献积分的L2叶子)
+            Map<Integer, String> contribL2LeafCatMap = new HashMap<>(); // i*1000+j -> categoryId
+            
+            for (int i = 0; i < secondLevelHeaders.size(); i++) {
+                List<String> l2ForL1 = secondLevelHeaders.get(i);
+                List<List<String>> l3ForL1 = thirdLevelHeaders.get(i);
+                for (int j = 0; j < l2ForL1.size(); j++) {
+                    List<String> l3ForL2 = l3ForL1.get(j);
+                    if (l3ForL2.isEmpty()) {
+                        // 无L3:可能是directRule、l2leaf、deduction、subtotal、exchange、remaining
+                        if (keyIdx < columnKeys.size()) {
+                            Map<String, String> km = columnKeys.get(keyIdx);
+                            String type = km.get("type");
+                            if ("directRule".equals(type) && i == 0) {
+                                // 基础积分的直接规则(如阅读、点赞)
+                                basicDirectRuleMap.put(j, km.get("ruleId"));
+                            } else if ("l2leaf".equals(type)) {
+                                if (i == 0) {
+                                    basicL2LeafCatMap.put(j, km.get("categoryId"));
+                                } else {
+                                    contribL2LeafSet.add(i * 1000 + j);
+                                    contribL2LeafCatMap.put(i * 1000 + j, km.get("categoryId"));
+                                }
+                            }
+                            // deduction/subtotal/exchange/remaining 不需要处理
+                            keyIdx++;
+                        }
+                    } else {
+                        // 有L3:遍历每个L3规则
+                        for (int k = 0; k < l3ForL2.size(); k++) {
+                            if (keyIdx < columnKeys.size()) {
+                                Map<String, String> km = columnKeys.get(keyIdx);
+                                if ("rule".equals(km.get("type"))) {
+                                    // 从key字段解析ruleId(格式:"rule_" + ruleId),与导出Controller一致
+                                    String key = km.get("key");
+                                    String ruleId = key != null && key.startsWith("rule_") ? key.substring(5) : "";
+                                    // 用 i*1000000 + j*1000 + k 作为唯一key
+                                    contribRuleMap.put(i * 1000000 + j * 1000 + k, ruleId);
+                                }
+                                keyIdx++;
+                            }
+                        }
+                    }
+                }
+            }
+
+            // 6. 计算基础积分各L2叶子分类的积分(从pointsByRule按规则汇总)
+            Map<String, Map<String, Integer>> basicL2PointsByUser = new HashMap<>();
+            for (Map.Entry<String, Map<String, Integer>> entry : pointsByRule.entrySet()) {
+                String uid = entry.getKey();
+                Map<String, Integer> userRulePoints = entry.getValue();
+                Map<String, Integer> l2Points = new LinkedHashMap<>();
+                // directRule类型
+                for (Map.Entry<Integer, String> drEntry : basicDirectRuleMap.entrySet()) {
+                    String ruleId = drEntry.getValue();
+                    int pts = userRulePoints.getOrDefault(ruleId, 0);
+                    l2Points.put("direct_" + drEntry.getKey(), pts);
+                }
+                // l2leaf类型
+                for (Map.Entry<Integer, String> l2Entry : basicL2LeafCatMap.entrySet()) {
+                    String catId = l2Entry.getValue();
+                    // 查询该L2分类下的所有规则ID并求和
+                    List<WorkKnowledgeBasePointRule> catRules = pointRuleDao.findEnabledByCategoryId(catId);
+                    int total = 0;
+                    for (WorkKnowledgeBasePointRule r : catRules) {
+                        total += userRulePoints.getOrDefault(r.getId(), 0);
+                    }
+                    l2Points.put("l2leaf_" + l2Entry.getKey(), total);
+                }
+                basicL2PointsByUser.put(uid, l2Points);
+            }
+
+            // 6b. 构建贡献积分 ruleId -> field编号 的反向映射(field3起)
+            Map<String, Integer> contribRuleIdToField = new HashMap<>();
+            int contribFieldIdx = 3;
+            for (Map.Entry<Integer, String> crEntry : contribRuleMap.entrySet()) {
+                contribRuleIdToField.put(crEntry.getValue(), contribFieldIdx++);
+            }
+            // 贡献积分L2叶子也分配field(如果有)
+            for (Map.Entry<Integer, String> cl2Entry : contribL2LeafCatMap.entrySet()) {
+                String catId = cl2Entry.getValue();
+                List<WorkKnowledgeBasePointRule> catRules = pointRuleDao.findEnabledByCategoryId(catId);
+                for (WorkKnowledgeBasePointRule r : catRules) {
+                    if (!contribRuleIdToField.containsKey(r.getId())) {
+                        contribRuleIdToField.put(r.getId(), contribFieldIdx++);
+                    }
+                }
+            }
+            logger.info("贡献积分规则数:{},field范围:3~{}", contribRuleIdToField.size(), contribFieldIdx - 1);
+            // 打印 contribRuleIdToField 映射详情,便于排查
+            for (Map.Entry<String, Integer> mappingEntry : contribRuleIdToField.entrySet()) {
+                logger.info("  贡献规则映射: ruleId={} -> field{}", mappingEntry.getKey(), mappingEntry.getValue());
+            }
+            // 打印第一个用户的 pointsByRule 数据,便于对比
+            if (!pointsByRule.isEmpty()) {
+                Map.Entry<String, Map<String, Integer>> firstUser = pointsByRule.entrySet().iterator().next();
+                logger.info("用户 {} 的积分规则数据:", firstUser.getKey());
+                for (Map.Entry<String, Integer> rp : firstUser.getValue().entrySet()) {
+                    Integer mappedField = contribRuleIdToField.get(rp.getKey());
+                    logger.info("  ruleId={}, 积分={}, 映射field={}", rp.getKey(), rp.getValue(), mappedField != null ? mappedField : "未匹配!");
+                }
+            }
+
+            // 7. 兑换积分
+            Map<String, Map<String, Integer>> exchangeByL1 = new HashMap<>();
+            if (!userIds.isEmpty()) {
+                List<Map<String, Object>> exResults =
+                        pointDetailService.sumExchangeByFirstLevelCategory(userIds, beginDate, endDate);
+                for (Map<String, Object> r : exResults) {
+                    String uid = (String) r.get("userId");
+                    String lid = String.valueOf(r.get("firstLevelId"));
+                    int pts = ((Number) r.get("totalPoint")).intValue();
+                    exchangeByL1.computeIfAbsent(uid, k -> new HashMap<>()).put(lid, pts);
+                }
+            }
+
+            // 获取L1分类ID
+            List<String> l1Ids = new ArrayList<>();
+            // 基础积分id=1, 贡献积分id=2
+            l1Ids.add("1");
+            l1Ids.add("2");
+
+            // 8. 上传文档数量
+            Map<String, Integer> uploadCountMap = new HashMap<>();
+            for (WorkKnowledgeBasePoint emp : employeeList) {
+                uploadCountMap.put(emp.getUserId(), emp.getUploadCount() != null ? emp.getUploadCount() : 0);
+            }
+
+            // 9. 为每个员工生成快照
+            Date now = new Date();
+            for (WorkKnowledgeBasePoint emp : employeeList) {
+                String uid = emp.getUserId();
+                WkPointSnapshotInfo info = new WkPointSnapshotInfo();
+                info.setId(IdGen.uuid());
+                info.setDelFlag("0");
+                info.setCreateDate(now);
+                info.setSnapshotYear(year);
+                info.setSnapshotMonth(month);
+                info.setOfficeId(emp.getOfficeId());
+                info.setOfficeName(emp.getOfficeName());
+                info.setUserId(uid);
+                info.setUserName(emp.getUserName());
+                info.setUploadCount(uploadCountMap.getOrDefault(uid, 0));
+
+                // field1~fieldN: 基础积分各L2叶子分类(如阅读、点赞)
+                Map<String, Integer> userBasicL2 = basicL2PointsByUser.getOrDefault(uid, Collections.emptyMap());
+                int basicSubtotal = 0;
+                int fieldIdx = 1;
+                for (Map.Entry<String, Integer> l2Entry : userBasicL2.entrySet()) {
+                    setField(info, fieldIdx, String.valueOf(l2Entry.getValue()));
+                    basicSubtotal += l2Entry.getValue();
+                    fieldIdx++;
+                }
+
+                // field3~fieldN: 贡献积分各规则(使用headerStructure中的实际ruleId)
+                Map<String, Integer> userRulePoints = pointsByRule.getOrDefault(uid, Collections.emptyMap());
+                int contribSubtotalFromRules = 0;
+                for (Map.Entry<String, Integer> ruleEntry : userRulePoints.entrySet()) {
+                    Integer fieldNum = contribRuleIdToField.get(ruleEntry.getKey());
+                    if (fieldNum != null && fieldNum >= 3) {
+                        setField(info, fieldNum, String.valueOf(ruleEntry.getValue()));
+                        contribSubtotalFromRules += ruleEntry.getValue();
+                    }
+                }
+
+                // field15: 扣除积分
+                info.setField15(String.valueOf(deductionByUser.getOrDefault(uid, 0)));
+
+                // 日志:第一个员工的field3-14值
+                if (employeeList.indexOf(emp) == 0) {
+                    logger.info("员工 {} 快照field值: field1={}, field2={}, field3={}, field4={}, field5={}, field6={}, field7={}, field8={}, field9={}, field10={}, field11={}, field12={}, field13={}, field14={}, field15={}",
+                        uid, info.getField1(), info.getField2(), info.getField3(), info.getField4(), info.getField5(), info.getField6(),
+                        info.getField7(), info.getField8(), info.getField9(), info.getField10(), info.getField11(), info.getField12(),
+                        info.getField13(), info.getField14(), info.getField15());
+                }
+
+                snapshotInfoDao.insert(info);
+
+                // 同时生成 summary 员工级记录
+                int contribSubtotal = contribSubtotalFromRules + deductionByUser.getOrDefault(uid, 0);
+                String basicL1Id = "1";
+                String contribL1Id = "2";
+                int basicExchange = getExchangeValue(exchangeByL1, uid, basicL1Id);
+                int contribExchange = getExchangeValue(exchangeByL1, uid, contribL1Id);
+
+                WkPointSnapshotSummary empSummary = new WkPointSnapshotSummary();
+                empSummary.setId(IdGen.uuid());
+                empSummary.setDelFlag("0");
+                empSummary.setCreateDate(now);
+                empSummary.setSnapshotYear(year);
+                empSummary.setSnapshotMonth(month);
+                empSummary.setGradeType("3");
+                empSummary.setOfficeId(emp.getOfficeId());
+                empSummary.setOfficeName(emp.getOfficeName());
+                empSummary.setUserId(uid);
+                empSummary.setUserName(emp.getUserName());
+                empSummary.setUploadCount(uploadCountMap.getOrDefault(uid, 0));
+                empSummary.setBasicSubtotal(basicSubtotal);
+                empSummary.setBasicExchange(basicExchange);
+                empSummary.setBasicRemaining(basicSubtotal - basicExchange);
+                empSummary.setContribSubtotal(contribSubtotal);
+                empSummary.setContribDeduction(deductionByUser.getOrDefault(uid, 0));
+                empSummary.setContribExchange(contribExchange);
+                empSummary.setContribRemaining(contribSubtotal - contribExchange);
+                snapshotSummaryDao.insert(empSummary);
+            }
+
+            // 10. 生成部门级汇总
+            Map<String, List<WorkKnowledgeBasePoint>> deptMap = new LinkedHashMap<>();
+            for (WorkKnowledgeBasePoint emp : employeeList) {
+                String dept = emp.getOfficeName() != null ? emp.getOfficeName() : "未分配部门";
+                deptMap.computeIfAbsent(dept, k -> new ArrayList<>()).add(emp);
+            }
+            for (Map.Entry<String, List<WorkKnowledgeBasePoint>> entry : deptMap.entrySet()) {
+                List<WorkKnowledgeBasePoint> deptEmps = entry.getValue();
+                String officeId = deptEmps.get(0).getOfficeId();
+
+                int totalUpload = deptEmps.stream().mapToInt(e -> uploadCountMap.getOrDefault(e.getUserId(), 0)).sum();
+                int totalBasicSubtotal = deptEmps.stream().mapToInt(e -> {
+                    Map<String, Integer> l2p = basicL2PointsByUser.getOrDefault(e.getUserId(), Collections.emptyMap());
+                    return l2p.values().stream().mapToInt(Integer::intValue).sum();
+                }).sum();
+                int totalContribSubtotal = deptEmps.stream().mapToInt(e -> {
+                    Map<String, Integer> urp = pointsByRule.getOrDefault(e.getUserId(), Collections.emptyMap());
+                    int sub = 0;
+                    for (Map.Entry<String, Integer> re : urp.entrySet()) {
+                        if (contribRuleIdToField.containsKey(re.getKey())) sub += re.getValue();
+                    }
+                    return sub + deductionByUser.getOrDefault(e.getUserId(), 0);
+                }).sum();
+                int totalBasicExchange = deptEmps.stream().mapToInt(e -> getExchangeValue(exchangeByL1, e.getUserId(), "1")).sum();
+                int totalContribExchange = deptEmps.stream().mapToInt(e -> getExchangeValue(exchangeByL1, e.getUserId(), "2")).sum();
+                int totalContribDeduction = deptEmps.stream().mapToInt(e -> deductionByUser.getOrDefault(e.getUserId(), 0)).sum();
+
+                WkPointSnapshotSummary deptSummary = new WkPointSnapshotSummary();
+                deptSummary.setId(IdGen.uuid());
+                deptSummary.setDelFlag("0");
+                deptSummary.setCreateDate(now);
+                deptSummary.setSnapshotYear(year);
+                deptSummary.setSnapshotMonth(month);
+                deptSummary.setGradeType("2");
+                deptSummary.setOfficeId(officeId);
+                deptSummary.setOfficeName(entry.getKey());
+                deptSummary.setUploadCount(totalUpload);
+                deptSummary.setBasicSubtotal(totalBasicSubtotal);
+                deptSummary.setBasicExchange(totalBasicExchange);
+                deptSummary.setBasicRemaining(totalBasicSubtotal - totalBasicExchange);
+                deptSummary.setContribSubtotal(totalContribSubtotal);
+                deptSummary.setContribDeduction(totalContribDeduction);
+                deptSummary.setContribExchange(totalContribExchange);
+                deptSummary.setContribRemaining(totalContribSubtotal - totalContribExchange);
+                snapshotSummaryDao.insert(deptSummary);
+            }
+
+            // 11. 生成公司级汇总
+            int companyUpload = employeeList.stream().mapToInt(e -> uploadCountMap.getOrDefault(e.getUserId(), 0)).sum();
+            int companyBasicSubtotal = employeeList.stream().mapToInt(e -> {
+                Map<String, Integer> l2p = basicL2PointsByUser.getOrDefault(e.getUserId(), Collections.emptyMap());
+                return l2p.values().stream().mapToInt(Integer::intValue).sum();
+            }).sum();
+            int companyContribSubtotal = employeeList.stream().mapToInt(e -> {
+                Map<String, Integer> urp = pointsByRule.getOrDefault(e.getUserId(), Collections.emptyMap());
+                int sub = 0;
+                for (Map.Entry<String, Integer> re : urp.entrySet()) {
+                    if (contribRuleIdToField.containsKey(re.getKey())) sub += re.getValue();
+                }
+                return sub + deductionByUser.getOrDefault(e.getUserId(), 0);
+            }).sum();
+            int companyBasicExchange = employeeList.stream().mapToInt(e -> getExchangeValue(exchangeByL1, e.getUserId(), "1")).sum();
+            int companyContribExchange = employeeList.stream().mapToInt(e -> getExchangeValue(exchangeByL1, e.getUserId(), "2")).sum();
+            int companyContribDeduction = employeeList.stream().mapToInt(e -> deductionByUser.getOrDefault(e.getUserId(), 0)).sum();
+
+            WkPointSnapshotSummary companySummary = new WkPointSnapshotSummary();
+            companySummary.setId(IdGen.uuid());
+            companySummary.setDelFlag("0");
+            companySummary.setCreateDate(now);
+            companySummary.setSnapshotYear(year);
+            companySummary.setSnapshotMonth(month);
+            companySummary.setGradeType("1");
+            companySummary.setUploadCount(companyUpload);
+            companySummary.setBasicSubtotal(companyBasicSubtotal);
+            companySummary.setBasicExchange(companyBasicExchange);
+            companySummary.setBasicRemaining(companyBasicSubtotal - companyBasicExchange);
+            companySummary.setContribSubtotal(companyContribSubtotal);
+            companySummary.setContribDeduction(companyContribDeduction);
+            companySummary.setContribExchange(companyContribExchange);
+            companySummary.setContribRemaining(companyContribSubtotal - companyContribExchange);
+            snapshotSummaryDao.insert(companySummary);
+
+            logger.info("-----------知识库积分快照生成({}年{}月)完成,共{}名员工------------------", year, month, employeeList.size());
+
+        } catch (Exception e) {
+            logger.error("-----------知识库积分快照生成失败------------------", e);
+            throw new RuntimeException("知识库积分快照生成失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 设置info的field值
+     */
+    private void setField(WkPointSnapshotInfo info, int fieldNum, String value) {
+        switch (fieldNum) {
+            case 1: info.setField1(value); break;
+            case 2: info.setField2(value); break;
+            case 3: info.setField3(value); break;
+            case 4: info.setField4(value); break;
+            case 5: info.setField5(value); break;
+            case 6: info.setField6(value); break;
+            case 7: info.setField7(value); break;
+            case 8: info.setField8(value); break;
+            case 9: info.setField9(value); break;
+            case 10: info.setField10(value); break;
+            case 11: info.setField11(value); break;
+            case 12: info.setField12(value); break;
+            case 13: info.setField13(value); break;
+            case 14: info.setField14(value); break;
+            case 15: info.setField15(value); break;
+            case 16: info.setField16(value); break;
+            case 17: info.setField17(value); break;
+            case 18: info.setField18(value); break;
+            case 19: info.setField19(value); break;
+            case 20: info.setField20(value); break;
+        }
+    }
+
+    /**
+     * 获取兑换积分值
+     */
+    private int getExchangeValue(Map<String, Map<String, Integer>> exchangeByL1, String userId, String l1Id) {
+        Map<String, Integer> userExchange = exchangeByL1.get(userId);
+        return userExchange != null && userExchange.containsKey(l1Id) ? userExchange.get(l1Id) : 0;
+    }
+
+    /**
+     * 发送知识库积分通知
+     * @param year 年份
+     * @param month 月份
+     */
+    @Transactional(readOnly = false)
+    public void sendNotification(Integer year, Integer month) {
+        logger.info("-----------知识库积分通知发送({}年{}月)开始------------------", year, month);
+
+        String notifyStr = year + "年" + month + "月-知识库积分统计";
+        String titleStr = year + "年" + month + "月-知识库积分统计";
+        String notifyId = "201," + year + "," + month;
+        String utilNotifyId = "201";
+
+        try {
+            // 查询需要通知的人员
+            List<WorkKnowledgePointNotifyUser> notifyUsers = notifyUserDao.findAllNotifyUsers();
+            if (notifyUsers == null || notifyUsers.isEmpty()) {
+                logger.info("-----------无通知人员,跳过通知发送------------------");
+                return;
+            }
+
+            for (WorkKnowledgePointNotifyUser notifyUser : notifyUsers) {
+                try {
+                    User user = UserUtils.get(notifyUser.getUserId());
+                    if (user != null) {
+                        workProjectNotifyService.save(
+                                UtilNotify.saveNotify(notifyId,
+                                        user,
+                                        user.getCompany().getId(),
+                                        titleStr,
+                                        notifyStr,
+                                        utilNotifyId,
+                                        "0",
+                                        "待通知",
+                                        "")
+                        );
+                    }
+                } catch (Exception e) {
+                    logger.warn("发送通知给用户{}失败:{}", notifyUser.getUserId(), e.getMessage());
+                }
+            }
+
+            logger.info("-----------知识库积分通知发送({}年{}月)完成,共{}人------------------", year, month, notifyUsers.size());
+        } catch (Exception e) {
+            logger.error("-----------知识库积分通知发送失败------------------", e);
+        }
+    }
+
+    /**
+     * 查询看板数据(按年月)
+     * 返回公司级+部门级数据
+     */
+    public List<WkPointSnapshotSummary> getDashboardData(Integer year, Integer month) {
+        return snapshotSummaryDao.findByYearMonth(year, month);
+    }
+
+    /**
+     * 查询所有员工快照明细(含field1-field15贡献积分明细)
+     */
+    public List<WkPointSnapshotInfo> getAllEmployeeInfos(Integer year, Integer month) {
+        return snapshotInfoDao.findByYearMonth(year, month);
+    }
+
+    /**
+     * 查询部门下员工数据(含field1-field15贡献积分明细)
+     */
+    public List<WkPointSnapshotInfo> getDepartmentEmployeeInfos(Integer year, Integer month, String officeId, String userId, String userName, String officeName) {
+        return snapshotInfoDao.findByYearMonthAndOffice(year, month, officeId, userId, userName, officeName);
+    }
+
+    /**
+     * 查询部门下员工数据(汇总级,不含明细)
+     */
+    public List<WkPointSnapshotSummary> getDepartmentEmployees(Integer year, Integer month, String officeId, String userId, String userName, String officeName) {
+        return snapshotSummaryDao.findEmployeeByYearMonthAndOffice(year, month, officeId, userId, userName, officeName);
+    }
+
+    /**
+     * 查询员工快照详情
+     */
+    public WkPointSnapshotInfo getEmployeeSnapshot(Integer year, Integer month, String userId) {
+        return snapshotInfoDao.findByYearMonthAndUser(year, month, userId);
+    }
+
+    /**
+     * 钻取查询:从快照明细表分页查询
+     */
+    public Page<WkPointSnapshotDetail> findDrillDownPage(Page<WkPointSnapshotDetail> page, WkPointSnapshotDetail query) {
+        int count = snapshotDetailDao.findDrillDownCount(query);
+        page.setCount(count);
+        page.setCountFlag(false);
+        query.setPage(page);
+        List<WkPointSnapshotDetail> list = snapshotDetailDao.findDrillDownList(query);
+        page.setList(list);
+        return page;
+    }
+}

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

@@ -204,4 +204,17 @@ public class WorkKnowledgeBasePointDetailService extends CrudService<WorkKnowled
         }
         return pointDetailDao.sumExchangeByFirstLevelCategory(userIds, beginDate, endDate);
     }
+
+    /**
+     * 钻取查询:按部门+L2分类+规则类型查询积分明细文件列表(分页)
+     */
+    public Page<UserPointDetail> findDrillDownPage(Page<UserPointDetail> page, UserPointDetail userPointDetail) {
+        int count = pointDetailDao.findDrillDownCount(userPointDetail);
+        page.setCount(count);
+        page.setCountFlag(false);
+        userPointDetail.setPage(page);
+        List<UserPointDetail> list = pointDetailDao.findDrillDownList(userPointDetail);
+        page.setList(list);
+        return page;
+    }
 }

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

@@ -275,6 +275,7 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         params.put("offset", (pageNo - 1) * pageSize);
         params.put("limit", pageSize);
 
+
         int count = dao.findDynamicCount(params);
         page.setCount(count);
         page.setCountFlag(false);

+ 14 - 3
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgePointCategoryService.java

@@ -219,7 +219,18 @@ public class WorkKnowledgePointCategoryService extends TreeService<WorkKnowledge
                 }
             }
             
-            // 7. 添加"积分汇总"列(明细列之后)
+            // 7. 添加"扣除积分"列(仅贡献积分组,在明细列之后、积分汇总之前)
+            if ("贡献积分".equals(firstLevel.getCategoryName())) {
+                secondLevelNames.add("扣除积分");
+                thirdLevelForThisFirst.add(new ArrayList<>());
+                Map<String, String> deductionKeyMap = new HashMap<>();
+                deductionKeyMap.put("key", "deduction_" + firstLevel.getId());
+                deductionKeyMap.put("type", "deduction");
+                deductionKeyMap.put("categoryName", firstLevel.getCategoryName());
+                columnKeys.add(deductionKeyMap);
+            }
+            
+            // 8. 添加"积分汇总"列(明细列之后)
             secondLevelNames.add("积分汇总");
             thirdLevelForThisFirst.add(new ArrayList<>());
             Map<String, String> subtotalKeyMap = new HashMap<>();
@@ -228,7 +239,7 @@ public class WorkKnowledgePointCategoryService extends TreeService<WorkKnowledge
             subtotalKeyMap.put("categoryName", firstLevel.getCategoryName());
             columnKeys.add(subtotalKeyMap);
             
-            // 8. 添加"已兑换积分"列(积分汇总之后)
+            // 9. 添加"已兑换积分"列(积分汇总之后)
             secondLevelNames.add("已兑换积分");
             thirdLevelForThisFirst.add(new ArrayList<>());
             Map<String, String> exchangeKeyMap = new HashMap<>();
@@ -237,7 +248,7 @@ public class WorkKnowledgePointCategoryService extends TreeService<WorkKnowledge
             exchangeKeyMap.put("categoryName", firstLevel.getCategoryName());
             columnKeys.add(exchangeKeyMap);
             
-            // 9. 添加"剩余积分"列(已兑换积分之后)
+            // 10. 添加"剩余积分"列(已兑换积分之后)
             secondLevelNames.add("剩余积分");
             thirdLevelForThisFirst.add(new ArrayList<>());
             Map<String, String> remainingKeyMap = new HashMap<>();

+ 361 - 40
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBasePointController.java

@@ -11,6 +11,9 @@ import com.jeeplus.common.utils.excel.ExportExcel;
 import com.jeeplus.common.utils.excel.ExportKnowledgeBaseExcel;
 import com.jeeplus.common.web.BaseController;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.UserPointDetail;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotDetail;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePointRule;
 import com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgeBasePointRuleDao;
@@ -19,6 +22,8 @@ import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointExcha
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBaseTreeService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgePointCategoryService;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgePointCategory;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WkPointSnapshotService;
 import com.jeeplus.modules.sys.entity.User;
 import com.jeeplus.modules.sys.service.OfficeService;
 import com.jeeplus.modules.sys.utils.UserUtils;
@@ -70,13 +75,16 @@ public class WorkKnowledgeBasePointController extends BaseController {
     @Autowired
     private WorkKnowledgeBasePointRuleDao pointRuleDao;
 
+    @Autowired
+    private WkPointSnapshotService wkPointSnapshotService;
+
     @ModelAttribute
-    public WorkKnowledgeBasePoint get(@RequestParam(required=false) String id) {
+    public WorkKnowledgeBasePoint get(@RequestParam(required = false) String id) {
         WorkKnowledgeBasePoint entity = null;
-        if (StringUtils.isNotBlank(id)){
+        if (StringUtils.isNotBlank(id)) {
             entity = workKnowledgeBasePointService.get(id);
         }
-        if (entity == null){
+        if (entity == null) {
             entity = new WorkKnowledgeBasePoint();
         }
         return entity;
@@ -89,8 +97,8 @@ public class WorkKnowledgeBasePointController extends BaseController {
     @RequestMapping(value = {"list", ""})
     public String list(WorkKnowledgeBasePoint workKnowledgeBasePoint, HttpServletRequest request, HttpServletResponse response, Model model) {
         //进行查询之后进行任何操作,返回还是查询之后的数据页面
-        if (StringUtils.isNotBlank(workKnowledgeBasePoint.getToflag())){
-            if (workKnowledgeBasePoint.getToflag().equals("1")){
+        if (StringUtils.isNotBlank(workKnowledgeBasePoint.getToflag())) {
+            if (workKnowledgeBasePoint.getToflag().equals("1")) {
                 request.getSession().removeAttribute("searchPoint");
                 WorkKnowledgeBasePoint searchPoint = workKnowledgeBasePoint;
                 request.getSession().setAttribute("searchPoint", searchPoint);
@@ -99,14 +107,14 @@ public class WorkKnowledgeBasePointController extends BaseController {
         //判断经办人是否为空,为空则赋值为null 防止查找为空的数据
         String userId = null;
         String userName = null;
-        if(StringUtils.isBlank(workKnowledgeBasePoint.getSubmitterName())){
+        if (StringUtils.isBlank(workKnowledgeBasePoint.getSubmitterName())) {
             workKnowledgeBasePoint.setSubmitterName(null);
             workKnowledgeBasePoint.setSubmitterId(null);
-        }else{
+        } else {
             //如果ID和名称都有值,说明是通过选择器选的精确查询
-            if(StringUtils.isNotBlank(workKnowledgeBasePoint.getSubmitterId())){
+            if (StringUtils.isNotBlank(workKnowledgeBasePoint.getSubmitterId())) {
                 userId = workKnowledgeBasePoint.getSubmitterId();
-            }else{
+            } else {
                 //如果经办人名称有值但ID为空,说明是手动输入的模糊查询
                 userName = workKnowledgeBasePoint.getSubmitterName();
             }
@@ -114,9 +122,9 @@ public class WorkKnowledgeBasePointController extends BaseController {
 
         User user = UserUtils.getUser();
         Subject subject = SecurityUtils.getSubject();
-        if(user.isAdmin()  || subject.isPermitted("workKnowledgeBase:point:listAll")){
+        if (user.isAdmin() || subject.isPermitted("workKnowledgeBase:point:listAll")) {
             workKnowledgeBasePoint.setIsAdmin("admin");
-        }else{
+        } else {
             workKnowledgeBasePoint.setIsAdmin("user");
             // 普通用户只能查看自己的积分,设置当前用户ID
             workKnowledgeBasePoint.setUserId(user.getId());
@@ -135,7 +143,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
     /**
      * 查看,增加,编辑知识库积分管理表单页面
      */
-    @RequiresPermissions(value={"workKnowledgeBase:point:view","workKnowledgeBase:point:add","workKnowledgeBase:point:edit"},logical= Logical.OR)
+    @RequiresPermissions(value = {"workKnowledgeBase:point:view", "workKnowledgeBase:point:add", "workKnowledgeBase:point:edit"}, logical = Logical.OR)
     @RequestMapping(value = "form")
     public String form(WorkKnowledgeBasePoint workKnowledgeBasePoint, Model model) {
         model.addAttribute("workKnowledgeBasePoint", workKnowledgeBasePoint);
@@ -145,16 +153,16 @@ public class WorkKnowledgeBasePointController extends BaseController {
     /**
      * 保存知识库积分管理
      */
-    @RequiresPermissions(value={"workKnowledgeBase:point:add","workKnowledgeBase:point:edit"},logical=Logical.OR)
+    @RequiresPermissions(value = {"workKnowledgeBase:point:add", "workKnowledgeBase:point:edit"}, logical = Logical.OR)
     @RequestMapping(value = "save")
-    public String save(WorkKnowledgeBasePoint workKnowledgeBasePoint, Model model, RedirectAttributes redirectAttributes) throws Exception{
-        if (!beanValidator(model, workKnowledgeBasePoint)){
+    public String save(WorkKnowledgeBasePoint workKnowledgeBasePoint, Model model, RedirectAttributes redirectAttributes) throws Exception {
+        if (!beanValidator(model, workKnowledgeBasePoint)) {
             return form(workKnowledgeBasePoint, model);
         }
 
         workKnowledgeBasePointService.save(workKnowledgeBasePoint);
         addMessage(redirectAttributes, "保存知识库积分管理成功");
-        return "redirect:"+ Global.getAdminPath()+"/workKnowledgeBase/point/?repage";
+        return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/point/?repage";
     }
 
     /**
@@ -166,7 +174,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
         workKnowledgeBasePointService.delete(workKnowledgeBasePoint);
         addMessage(redirectAttributes, "删除知识库积分管理成功");
 
-        return "redirect:"+Global.getAdminPath()+"/workKnowledgeBase/point/?repage";
+        return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/point/?repage";
     }
 
 
@@ -186,7 +194,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
         Page<UserPointDetail> page = workKnowledgeBasePointDetailService.findUserPointDetailPage(new Page<>(request, response), userPointDetail);
         // 查询该用户在当前分类(含子节点)下的积分总和(根据积分类型筛选)
         int totalPoint = workKnowledgeBasePointDetailService.sumUserPointByColumnId(
-            userPointDetail.getUserId(), userPointDetail.getColumnId(), userPointDetail.getPointType());
+                userPointDetail.getUserId(), userPointDetail.getColumnId(), userPointDetail.getPointType());
         model.addAttribute("page", page);
         model.addAttribute("userPointDetail", userPointDetail);
         model.addAttribute("totalPoint", totalPoint);
@@ -195,12 +203,13 @@ public class WorkKnowledgeBasePointController extends BaseController {
 
     /**
      * 预览兑换明细(只读,不写DB,返回JSON)
+     *
      * @param exchangeType 兑换类型:basic-基础积分, contribution-贡献积分
      */
     @RequiresPermissions("workKnowledgeBase:point:listAll")
     @RequestMapping(value = "previewExchange")
     @ResponseBody
-    public Map<String, Object> previewExchange(@RequestParam(required=false) String exchangeType) {
+    public Map<String, Object> previewExchange(@RequestParam(required = false) String exchangeType) {
         Map<String, Object> result = new HashMap<>();
         try {
             if (StringUtils.isBlank(exchangeType)) {
@@ -220,11 +229,12 @@ public class WorkKnowledgeBasePointController extends BaseController {
 
     /**
      * 预览兑换明细页面(iframe弹窗用)
+     *
      * @param exchangeType 兑换类型:basic-基础积分, contribution-贡献积分
      */
     @RequiresPermissions("workKnowledgeBase:point:listAll")
     @RequestMapping(value = "previewExchangePage")
-    public String previewExchangePage(@RequestParam(required=false) String exchangeType, Model model) {
+    public String previewExchangePage(@RequestParam(required = false) String exchangeType, Model model) {
         if (StringUtils.isBlank(exchangeType)) {
             exchangeType = "basic";
         }
@@ -241,20 +251,21 @@ public class WorkKnowledgeBasePointController extends BaseController {
 
     /**
      * 批量兑换积分并导出Excel
+     *
      * @param exchangeType 兑换类型:basic-基础积分, contribution-贡献积分
      */
     @RequiresPermissions("workKnowledgeBase:point:listAll")
     @RequestMapping(value = "exportExchange")
-    public String exportExchange(@RequestParam(required=false) String exchangeType,
+    public String exportExchange(@RequestParam(required = false) String exchangeType,
                                  HttpServletResponse response, RedirectAttributes redirectAttributes) {
         try {
             // 默认兑换基础积分
             if (StringUtils.isBlank(exchangeType)) {
                 exchangeType = "basic";
             }
-            
+
             System.out.println("========== 开始导出积分兑换,类型:" + exchangeType + " ==========");
-            
+
             // 执行批量兑换并获取导出数据
             List<WorkKnowledgeBasePoint> exportList = pointExchangeService.batchExchangeForExport(exchangeType);
             System.out.println("========== 导出数据条数:" + (exportList != null ? exportList.size() : 0) + " ==========");
@@ -287,7 +298,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
                 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);
-                
+
                 // 根据兑换类型添加对应列
                 if ("basic".equals(exchangeType)) {
                     // 基础积分兑换:显示可兑换积分、基础积分兑换量、兑换后剩余积分
@@ -346,6 +357,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
             Set<Integer> directRuleIndices = new HashSet<>(); // 直接规则L2(无子分类的L1下的规则)
             Set<Integer> subtotalIndices = new HashSet<>(); // 汇总列索引
             Set<Integer> remainingIndices = new HashSet<>(); // 未兑换积分列索引
+            Set<Integer> deductionIndices = new HashSet<>(); // 扣除积分列索引(仅贡献积分组)
             Map<Integer, Integer> exchangeColumnIndexMap = new HashMap<>(); // L1索引 -> 已兑换积分列的j索引
             List<List<String>> secondLevelCategoryIds = new ArrayList<>(); // 每个L1对应的L2 categoryId列表
             List<List<String>> directRuleIds = new ArrayList<>(); // 每个L1对应的直接规则ruleId列表
@@ -387,6 +399,10 @@ public class WorkKnowledgeBasePointController extends BaseController {
                                 remainingIndices.add(i * 1000 + j);
                                 catIdsForL1.add("");
                                 drIdsForL1.add("");
+                            } else if ("deduction".equals(km.get("type"))) {
+                                deductionIndices.add(i * 1000 + j);
+                                catIdsForL1.add("");
+                                drIdsForL1.add("");
                             } else {
                                 // exchange类型,记录该L1组的exchange列索引
                                 exchangeColumnIndexMap.put(i, j);
@@ -417,20 +433,28 @@ public class WorkKnowledgeBasePointController extends BaseController {
 
             // 3. 收集用户ID
             List<String> userIds = employeeList.stream()
-                .map(WorkKnowledgeBasePoint::getUserId)
-                .filter(StringUtils::isNotBlank)
-                .collect(Collectors.toList());
+                    .map(WorkKnowledgeBasePoint::getUserId)
+                    .filter(StringUtils::isNotBlank)
+                    .collect(Collectors.toList());
 
-            // 4a. 按rule分组积分(point>0
+            // 4a. 按rule分组积分(包含负数扣分
             java.util.Map<String, java.util.Map<String, Integer>> pointsByRule = new java.util.HashMap<>();
+            // 扣分标准(category_id=10)的积分单独汇总,归入贡献积分小计
+            java.util.Map<String, Integer> deductionByUser = new java.util.HashMap<>();
             if (!userIds.isEmpty()) {
                 List<java.util.Map<String, Object>> ruleResults =
-                    workKnowledgeBasePointDetailService.sumPointsByRuleGrouped(userIds, queryParam.getBeginDate(), queryParam.getEndDate());
+                        workKnowledgeBasePointDetailService.sumPointsByRuleGrouped(userIds, queryParam.getBeginDate(), queryParam.getEndDate());
                 for (java.util.Map<String, Object> r : ruleResults) {
                     String uid = (String) r.get("userId");
+                    String catId = String.valueOf(r.get("categoryId"));
                     String rid = String.valueOf(r.get("ruleId"));
                     int pts = ((Number) r.get("totalPoint")).intValue();
-                    pointsByRule.computeIfAbsent(uid, k -> new java.util.HashMap<>()).put(rid, pts);
+                    if ("10".equals(catId)) {
+                        // 扣分标准的积分单独汇总
+                        deductionByUser.merge(uid, pts, Integer::sum);
+                    } else {
+                        pointsByRule.computeIfAbsent(uid, k -> new java.util.HashMap<>()).put(rid, pts);
+                    }
                 }
             }
 
@@ -459,7 +483,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
             java.util.Map<String, java.util.Map<String, Integer>> exchangeByL1 = new java.util.HashMap<>();
             if (!userIds.isEmpty()) {
                 List<java.util.Map<String, Object>> exResults =
-                    workKnowledgeBasePointDetailService.sumExchangeByFirstLevelCategory(userIds, queryParam.getBeginDate(), queryParam.getEndDate());
+                        workKnowledgeBasePointDetailService.sumExchangeByFirstLevelCategory(userIds, queryParam.getBeginDate(), queryParam.getEndDate());
                 for (java.util.Map<String, Object> r : exResults) {
                     String uid = (String) r.get("userId");
                     String lid = String.valueOf(r.get("firstLevelId"));
@@ -498,7 +522,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
                             if (directRuleIndices.contains(i * 1000 + j)) {
                                 // 直接规则L2(如"基础积分"下的规则),直接从pointsByRule取数据
                                 String ruleId = (i < directRuleIds.size() && j < directRuleIds.get(i).size())
-                                    ? directRuleIds.get(i).get(j) : "";
+                                        ? directRuleIds.get(i).get(j) : "";
                                 int drVal = emps.stream().mapToInt(e -> {
                                     java.util.Map<String, Integer> m = pointsByRule.get(e.getUserId());
                                     return m != null && m.containsKey(ruleId) ? m.get(ruleId) : 0;
@@ -507,7 +531,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
                             } else if (l2LeafIndices.contains(i * 1000 + j)) {
                                 // L2叶子列(如"阅读"、"点赞"),使用L2级汇总数据
                                 String catId = (i < secondLevelCategoryIds.size() && j < secondLevelCategoryIds.get(i).size())
-                                    ? secondLevelCategoryIds.get(i).get(j) : "";
+                                        ? secondLevelCategoryIds.get(i).get(j) : "";
                                 int leafVal = emps.stream().mapToInt(e -> {
                                     java.util.Map<String, Integer> m = l2PointsByCategory.get(e.getUserId());
                                     return m != null && m.containsKey(catId) ? m.get(catId) : 0;
@@ -519,6 +543,12 @@ public class WorkKnowledgeBasePointController extends BaseController {
                             } else if (remainingIndices.contains(i * 1000 + j)) {
                                 // 未兑换积分列(占位符,后面计算 subtotal - exchange)
                                 row.put("remaining_" + i, 0);
+                            } else if (deductionIndices.contains(i * 1000 + j)) {
+                                // 扣除积分列(仅贡献积分组),从deductionByUser取数据
+                                int dedVal = emps.stream().mapToInt(e ->
+                                        deductionByUser.getOrDefault(e.getUserId(), 0)
+                                ).sum();
+                                row.put("exchange_" + i + "_" + j, dedVal);
                             } else {
                                 // 已兑换积分列
                                 int exVal = emps.stream().mapToInt(e -> {
@@ -530,8 +560,8 @@ public class WorkKnowledgeBasePointController extends BaseController {
                         } else {
                             for (int k = 0; k < l3ForL2.size(); k++) {
                                 String ruleKey = (i < dataKeysL3.size() && j < dataKeysL3.get(i).size()
-                                    && k < dataKeysL3.get(i).get(j).size())
-                                    ? dataKeysL3.get(i).get(j).get(k) : "";
+                                        && k < dataKeysL3.get(i).get(j).size())
+                                        ? dataKeysL3.get(i).get(j).get(k) : "";
                                 String ruleId = ruleKey.startsWith("rule_") ? ruleKey.substring(5) : "";
                                 int ruleVal = emps.stream().mapToInt(e -> {
                                     java.util.Map<String, Integer> m = pointsByRule.get(e.getUserId());
@@ -548,8 +578,8 @@ public class WorkKnowledgeBasePointController extends BaseController {
                         if (subtotalIndices.contains(i * 1000 + j) || remainingIndices.contains(i * 1000 + j)) {
                             // 跳过汇总列和未兑换积分列本身
                             continue;
-                        } else if (directRuleIndices.contains(i * 1000 + j) || l2LeafIndices.contains(i * 1000 + j)) {
-                            // 直接规则L2或L2叶子列,直接取值
+                        } else if (directRuleIndices.contains(i * 1000 + j) || l2LeafIndices.contains(i * 1000 + j) || deductionIndices.contains(i * 1000 + j)) {
+                            // 直接规则L2、L2叶子列、扣除积分列,直接取值
                             Object v = row.get("exchange_" + i + "_" + j);
                             if (v instanceof Number) subtotal += ((Number) v).intValue();
                         } else if (!l3ForL2.isEmpty()) {
@@ -584,7 +614,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
             companyRow.put("label", "公司汇总");
             companyData.add(companyRow);
             excel.addTripleHeaderSheet("公司级", "公司级积分统计汇总" + dateRangeSuffix,
-                prefixHeadersCompany, firstLevelHeaders, secondLevelHeaders, thirdLevelHeaders, companyData);
+                    prefixHeadersCompany, firstLevelHeaders, secondLevelHeaders, thirdLevelHeaders, companyData);
 
             // ========== 2. 部门级汇总 ==========
             Map<String, List<WorkKnowledgeBasePoint>> deptMap = new LinkedHashMap<>();
@@ -599,7 +629,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
                 deptData.add(row);
             }
             excel.addTripleHeaderSheet("部门级", "部门级积分统计汇总" + dateRangeSuffix,
-                prefixHeadersDept, firstLevelHeaders, secondLevelHeaders, thirdLevelHeaders, deptData);
+                    prefixHeadersDept, firstLevelHeaders, secondLevelHeaders, thirdLevelHeaders, deptData);
 
             // ========== 3. 员工级明细 ==========
             List<Map<String, Object>> empData = new ArrayList<>();
@@ -610,7 +640,7 @@ public class WorkKnowledgeBasePointController extends BaseController {
                 empData.add(row);
             }
             excel.addTripleHeaderSheet("员工级", "员工级积分统计明细" + dateRangeSuffix,
-                prefixHeadersAll, firstLevelHeaders, secondLevelHeaders, thirdLevelHeaders, empData);
+                    prefixHeadersAll, firstLevelHeaders, secondLevelHeaders, thirdLevelHeaders, empData);
 
             String fileName = "知识库统计表_" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
             excel.write(response, fileName);
@@ -621,4 +651,295 @@ public class WorkKnowledgeBasePointController extends BaseController {
         }
     }
 
+    /**
+     * 知识库积分看板页面(从知识库管理菜单进入,可选择月份)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:list")
+    @RequestMapping(value = "dashboard")
+    public String dashboard(Model model) {
+        // 默认显示上个月
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.MONTH, -1);
+        int defaultYear = cal.get(Calendar.YEAR);
+        int defaultMonth = cal.get(Calendar.MONTH) + 1;
+        model.addAttribute("defaultYear", defaultYear);
+        model.addAttribute("defaultMonth", defaultMonth);
+        return "modules/WorkKnowledgeBase/workKnowledgeBasePointDashboard";
+    }
+
+    /**
+     * 看板数据接口(公司级+部门级汇总)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:list")
+    @RequestMapping(value = "dashboardData")
+    @ResponseBody
+    public Map<String, Object> dashboardData(Integer year, Integer month) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<WkPointSnapshotSummary> dataList = wkPointSnapshotService.getDashboardData(year, month);
+            // 分离公司级和部门级数据
+            List<WkPointSnapshotSummary> companyData = new ArrayList<>();
+            List<WkPointSnapshotSummary> deptData = new ArrayList<>();
+            for (WkPointSnapshotSummary item : dataList) {
+                if ("1".equals(item.getGradeType())) {
+                    companyData.add(item);
+                } else if ("2".equals(item.getGradeType())) {
+                    deptData.add(item);
+                }
+            }
+
+            // 查询所有员工快照明细,聚合贡献积分各列(field3-field14)
+            List<WkPointSnapshotInfo> allInfos = wkPointSnapshotService.getAllEmployeeInfos(year, month);
+            // 公司级聚合(field1~field15全部聚合)
+            Map<String, Integer> companyContrib = new LinkedHashMap<>();
+            for (int f = 1; f <= 15; f++) companyContrib.put("field" + f, 0);
+            for (WkPointSnapshotInfo info : allInfos) {
+                for (int f = 1; f <= 15; f++) {
+                    String val = getFieldValue(info, f);
+                    companyContrib.merge("field" + f, parseInt(val), Integer::sum);
+                }
+            }
+            // 部门级聚合(按officeId分组)
+            Map<String, Map<String, Integer>> deptContribMap = new LinkedHashMap<>();
+            for (WkPointSnapshotInfo info : allInfos) {
+                String oid = info.getOfficeId() != null ? info.getOfficeId() : "";
+                Map<String, Integer> deptContrib = deptContribMap.computeIfAbsent(oid, k -> {
+                    Map<String, Integer> m = new LinkedHashMap<>();
+                    for (int f = 1; f <= 15; f++) m.put("field" + f, 0);
+                    return m;
+                });
+                for (int f = 1; f <= 15; f++) {
+                    String val = getFieldValue(info, f);
+                    deptContrib.merge("field" + f, parseInt(val), Integer::sum);
+                }
+            }
+
+            result.put("success", true);
+            result.put("companyData", companyData);
+            result.put("deptData", deptData);
+            result.put("companyContrib", companyContrib);
+            result.put("deptContribMap", deptContribMap);
+
+            // 构建L2分类名称→ID映射,供前端钻取查询使用
+            Map<String, String> l2CategoryIdMap = new LinkedHashMap<>();
+            List<WorkKnowledgePointCategory> firstLevelList = workKnowledgePointCategoryService.findFirstLevelList();
+            for (WorkKnowledgePointCategory fl : firstLevelList) {
+                List<WorkKnowledgePointCategory> children = workKnowledgePointCategoryService.findChildrenByParentId(fl.getId());
+                for (WorkKnowledgePointCategory child : children) {
+                    l2CategoryIdMap.put(child.getCategoryName(), child.getId());
+                }
+            }
+            result.put("l2CategoryIdMap", l2CategoryIdMap);
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+        return result;
+    }
+
+    private String getFieldValue(WkPointSnapshotInfo info, int fieldNum) {
+        switch (fieldNum) {
+            case 1: return info.getField1();
+            case 2: return info.getField2();
+            case 3: return info.getField3();
+            case 4: return info.getField4();
+            case 5: return info.getField5();
+            case 6: return info.getField6();
+            case 7: return info.getField7();
+            case 8: return info.getField8();
+            case 9: return info.getField9();
+            case 10: return info.getField10();
+            case 11: return info.getField11();
+            case 12: return info.getField12();
+            case 13: return info.getField13();
+            case 14: return info.getField14();
+            case 15: return info.getField15();
+            default: return "0";
+        }
+    }
+
+    private int parseInt(String val) {
+        if (val == null || val.isEmpty()) return 0;
+        try { return Integer.parseInt(val); } catch (NumberFormatException e) { return 0; }
+    }
+
+    /**
+     * 部门员工明细子页面(iframe弹窗用)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:list")
+    @RequestMapping(value = "deptEmployeesPage")
+    public String deptEmployeesPage(Integer year, Integer month, String officeId, String officeName,
+                                     String submitterId, String submitterName,
+                                     String searchOfficeId, String searchOfficeName,
+                                     Model model) {
+        // 树形部门选择器覆盖初始officeId
+        String effectiveOfficeId = StringUtils.isNotBlank(searchOfficeId) ? searchOfficeId : officeId;
+        String effectiveOfficeName = StringUtils.isNotBlank(searchOfficeName) ? searchOfficeName : officeName;
+        // 人员筛选:优先用ID精确匹配,否则用名称模糊查询
+        String filterUserId = StringUtils.isNotBlank(submitterId) ? submitterId : null;
+        String filterUserName = StringUtils.isNotBlank(submitterId) ? null : submitterName;
+        List<WkPointSnapshotInfo> empList = wkPointSnapshotService.getDepartmentEmployeeInfos(year, month, effectiveOfficeId, filterUserId, filterUserName, null);
+        List<WkPointSnapshotSummary> summaryList = wkPointSnapshotService.getDepartmentEmployees(year, month, effectiveOfficeId, filterUserId, filterUserName, null);
+        // 按userId构建Summary映射,方便JSP通过员工field取汇总值
+        Map<String, WkPointSnapshotSummary> summaryMap = new LinkedHashMap<>();
+        for (WkPointSnapshotSummary s : summaryList) {
+            summaryMap.put(s.getUserId(), s);
+        }
+        model.addAttribute("empList", empList);
+        model.addAttribute("summaryMap", summaryMap);
+        model.addAttribute("year", year);
+        model.addAttribute("month", month);
+        model.addAttribute("officeId", effectiveOfficeId != null ? effectiveOfficeId : "");
+        model.addAttribute("officeName", effectiveOfficeName != null ? effectiveOfficeName : "");
+        model.addAttribute("submitterId", submitterId != null ? submitterId : "");
+        model.addAttribute("submitterName", submitterName != null ? submitterName : "");
+        model.addAttribute("searchOfficeId", searchOfficeId != null ? searchOfficeId : "");
+        model.addAttribute("searchOfficeName", searchOfficeName != null ? searchOfficeName : "");
+        // 计算日期范围用于显示
+        if (year != null && month != null) {
+            String beginDate = String.format("%04d-%02d-01", year, month);
+            Calendar cal = Calendar.getInstance();
+            cal.set(year, month - 1, 1);
+            int maxDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
+            String endDate = String.format("%04d-%02d-%02d", year, month, maxDay);
+            model.addAttribute("beginDate", beginDate);
+            model.addAttribute("endDate", endDate);
+        }
+        // L2分类名称→ID映射,供员工明细页钻取使用
+        Map<String, String> l2CategoryIdMap = new LinkedHashMap<>();
+        List<WorkKnowledgePointCategory> firstLevelList = workKnowledgePointCategoryService.findFirstLevelList();
+        for (WorkKnowledgePointCategory fl : firstLevelList) {
+            List<WorkKnowledgePointCategory> children = workKnowledgePointCategoryService.findChildrenByParentId(fl.getId());
+            for (WorkKnowledgePointCategory child : children) {
+                l2CategoryIdMap.put(child.getCategoryName(), child.getId());
+            }
+        }
+        model.addAttribute("l2CategoryIdMap", l2CategoryIdMap);
+        return "modules/WorkKnowledgeBase/workKnowledgeBasePointDeptEmployees";
+    }
+
+    /**
+     * 部门下员工数据接口(返回快照明细,含field3-field14贡献积分明细)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:list")
+    @RequestMapping(value = "departmentEmployees")
+    @ResponseBody
+    public Map<String, Object> departmentEmployees(Integer year, Integer month, String officeId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            List<WkPointSnapshotInfo> empList = wkPointSnapshotService.getDepartmentEmployeeInfos(year, month, officeId, null, null, null);
+            result.put("success", true);
+            result.put("data", empList);
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 员工快照详情接口
+     */
+    @RequiresPermissions("workKnowledgeBase:point:list")
+    @RequestMapping(value = "employeeSnapshot")
+    @ResponseBody
+    public Map<String, Object> employeeSnapshot(Integer year, Integer month, String userId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            WkPointSnapshotInfo info = wkPointSnapshotService.getEmployeeSnapshot(year, month, userId);
+            result.put("success", true);
+            result.put("data", info);
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 积分钻取明细页面(从快照明细表查询,确保与看板数字一致)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:list")
+    @RequestMapping(value = "pointDrillDown")
+    public String pointDrillDown(Integer year, Integer month, String officeId, String officeName, String userId,
+                                  String categoryId, String categoryName, Integer ruleType,
+                                  String columnId, String shareName,
+                                  String submitterId, String submitterName,
+                                  String searchOfficeId, String searchOfficeName,
+                                  HttpServletRequest request, HttpServletResponse response, Model model) {
+        WkPointSnapshotDetail query = new WkPointSnapshotDetail();
+        query.setSnapshotYear(year);
+        query.setSnapshotMonth(month);
+        // 树形选择器覆盖初始值
+        String effectiveOfficeId = StringUtils.isNotBlank(searchOfficeId) ? searchOfficeId : (officeId != null ? officeId : "");
+        String effectiveUserId = StringUtils.isNotBlank(submitterId) ? submitterId : userId;
+        query.setOfficeId(effectiveOfficeId);
+        query.setUserId(effectiveUserId);
+        query.setCategoryId(categoryId);
+        query.setRuleType(ruleType);
+        // columnId="deduction"表示扣除积分钻取,SQL会过滤负积分
+        if ("deduction".equals(columnId)) {
+            query.setColumnId("deduction");
+        }
+        // 文档名称模糊查询
+        if (StringUtils.isNotBlank(shareName)) {
+            query.setShareNameQuery(shareName);
+        }
+        // 人员名称回退:当未选择用户但有输入名称时,用名称模糊查询
+        if (StringUtils.isBlank(submitterId) && StringUtils.isNotBlank(submitterName)) {
+            query.setUserNameQuery(submitterName);
+        }
+        // 部门名称回退:当未选择部门但有输入名称时,用名称模糊查询
+        if (StringUtils.isBlank(searchOfficeId) && StringUtils.isNotBlank(searchOfficeName)) {
+            query.setOfficeNameQuery(searchOfficeName);
+        }
+        Page<WkPointSnapshotDetail> page = wkPointSnapshotService.findDrillDownPage(
+                new Page<>(request, response), query);
+        model.addAttribute("page", page);
+        model.addAttribute("year", year);
+        model.addAttribute("month", month);
+        model.addAttribute("officeId", effectiveOfficeId);
+        model.addAttribute("officeName", officeName != null ? officeName : "");
+        model.addAttribute("userId", effectiveUserId != null ? effectiveUserId : "");
+        model.addAttribute("categoryId", categoryId);
+        model.addAttribute("categoryName", categoryName != null ? categoryName : "");
+        model.addAttribute("ruleType", ruleType);
+        model.addAttribute("columnId", columnId);
+        model.addAttribute("shareName", shareName != null ? shareName : "");
+        model.addAttribute("submitterId", submitterId != null ? submitterId : "");
+        model.addAttribute("submitterName", submitterName != null ? submitterName : "");
+        model.addAttribute("searchOfficeId", searchOfficeId != null ? searchOfficeId : "");
+        model.addAttribute("searchOfficeName", searchOfficeName != null ? searchOfficeName : "");
+        // 计算日期范围用于显示
+        if (year != null && month != null) {
+            String beginDate = String.format("%04d-%02d-01", year, month);
+            Calendar cal = Calendar.getInstance();
+            cal.set(year, month - 1, 1);
+            int maxDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
+            String endDate = String.format("%04d-%02d-%02d", year, month, maxDay);
+            model.addAttribute("beginDate", beginDate);
+            model.addAttribute("endDate", endDate);
+        }
+        return "modules/WorkKnowledgeBase/workKnowledgeBasePointDrillDown";
+    }
+
+    /**
+     * 手动触发快照生成(供管理员使用)
+     */
+    @RequiresPermissions("workKnowledgeBase:point:listAll")
+    @RequestMapping(value = "generateSnapshot")
+    @ResponseBody
+    public Map<String, Object> generateSnapshot(Integer year, Integer month) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            wkPointSnapshotService.generateSnapshot(year, month);
+            result.put("success", true);
+            result.put("message", year + "年" + month + "月快照数据生成成功");
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", "生成失败:" + e.getMessage());
+        }
+        return result;
+    }
+
 }

+ 34 - 0
src/main/java/com/jeeplus/modules/ruralprojectrecords/web/RuralProjectSignatureOldMessageDisposeController.java

@@ -9,6 +9,7 @@ import com.jeeplus.common.utils.*;
 import com.jeeplus.common.utils.excel.ExportExcel;
 import com.jeeplus.common.utils.excel.ImportExcel;
 import com.jeeplus.common.web.BaseController;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WkPointSnapshotService;
 import com.jeeplus.modules.militaryIndustryConfidentiality.service.MilitaryIndustryConfidentialityService;
 import com.jeeplus.modules.projectAccessory.dao.ProjectTemplateDao;
 import com.jeeplus.modules.projectAccessory.entity.ProjectTemplateInfo;
@@ -130,6 +131,9 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
     private OMSDisposeService omsDisposeService;
 
     @Autowired
+    private WkPointSnapshotService wkPointSnapshotService;
+
+    @Autowired
     private WorkStaffCertificateService workStaffCertificateService;
 
     private static final String HTTPTOP = Global.getConfig("signature_http_top");
@@ -1277,4 +1281,34 @@ public class RuralProjectSignatureOldMessageDisposeController extends BaseContro
         logger.info("-----------一级造价师人员证书过期通知操作定时任务结束------------------");
     }
 
+    /**
+     * 报表处理
+     * @return
+     */
+    @RequestMapping(value = "/getKnowledgeBasePointSnapshotOnMonth")
+    @ResponseBody
+    @Transactional(readOnly = false)
+    public Map<String,Object> getKnowledgeBasePointSnapshotOnMonth(){
+        Map<String,Object> map = new HashMap<>();
+        logger.info("-----------知识库积分月度快照生成及通知(开始)------------------");
+        final Calendar c = Calendar.getInstance();
+        // 计算上个月的年份和月份
+        c.add(Calendar.MONTH, -0);
+        Integer year = c.get(Calendar.YEAR);
+        Integer month = c.get(Calendar.MONTH) + 1; // Calendar.MONTH从0开始
+
+        try {
+            // 1. 生成上月快照数据
+            wkPointSnapshotService.generateSnapshot(year, month);
+            // 2. 发送通知
+            wkPointSnapshotService.sendNotification(year, month);
+        } catch (Exception e) {
+            logger.error("-----------知识库积分月度快照生成及通知失败------------------", e);
+        }
+
+        logger.info("-----------知识库积分月度快照生成及通知(结束)------------------");
+        map.put("msg","处理完成");
+        return map;
+    }
+
 }

+ 30 - 0
src/main/java/com/jeeplus/modules/workcalendar/service/WorkCalendarTaskService.java

@@ -4,6 +4,7 @@
 package com.jeeplus.modules.workcalendar.service;
 
 import com.jeeplus.common.utils.DateUtils;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WkPointSnapshotService;
 import com.jeeplus.modules.militaryIndustryConfidentiality.service.MilitaryIndustryConfidentialityService;
 import com.jeeplus.modules.ruralprojectrecords.service.RuralProjectRecordsService;
 import com.jeeplus.modules.ruralprojectrecords.service.RuralProjectSignatureOldMessageDisposeService;
@@ -81,6 +82,9 @@ public class WorkCalendarTaskService  {
     @Autowired
     private WorkStaffCertificateService workStaffCertificateService;
 
+    @Autowired
+    private WkPointSnapshotService wkPointSnapshotService;
+
     //@Scheduled(cron= "0 0/1 * * * ?")
     public void notifyTask() {
         logger.info("-----------定时任务开始------------------");
@@ -666,4 +670,30 @@ public class WorkCalendarTaskService  {
 
         logger.info("-----------一级造价师人员证书过期通知操作定时任务结束------------------");
     }
+
+    /**
+     * 知识库积分月度快照生成及通知
+     * 每月1号凌晨5点执行,生成上个月积分快照并发送通知
+     */
+    @Scheduled(cron= "0 0 5 1 * ?")
+    @Transactional(readOnly = false)
+    public void getKnowledgeBasePointSnapshotOnMonth() {
+        logger.info("-----------知识库积分月度快照生成及通知(开始)------------------");
+        final Calendar c = Calendar.getInstance();
+        // 计算上个月的年份和月份
+        c.add(Calendar.MONTH, -1);
+        Integer year = c.get(Calendar.YEAR);
+        Integer month = c.get(Calendar.MONTH) + 1; // Calendar.MONTH从0开始
+
+        try {
+            // 1. 生成上月快照数据
+            wkPointSnapshotService.generateSnapshot(year, month);
+            // 2. 发送通知
+            wkPointSnapshotService.sendNotification(year, month);
+        } catch (Exception e) {
+            logger.error("-----------知识库积分月度快照生成及通知失败------------------", e);
+        }
+
+        logger.info("-----------知识库积分月度快照生成及通知(结束)------------------");
+    }
 }

+ 19 - 0
src/main/java/com/jeeplus/modules/workprojectnotify/web/WorkProjectNotifyController.java

@@ -24,6 +24,7 @@ import com.jeeplus.modules.WorkKnowledgeBase.entity.*;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointRuleService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBaseReadLikeService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBaseShareService;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WkPointSnapshotService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeExpertService;
 import com.jeeplus.modules.act.entity.Act;
 import com.jeeplus.modules.act.service.ActTaskService;
@@ -646,6 +647,9 @@ public class WorkProjectNotifyController extends BaseController {
 	@Autowired
 	private WorkKnowledgeBaseReadLikeService readLikeService;
 
+	@Autowired
+	private WkPointSnapshotService wkPointSnapshotService;
+
 
 	@ModelAttribute
 	public WorkProjectNotify get(@RequestParam(required = false) String id) {
@@ -8287,6 +8291,21 @@ public class WorkProjectNotifyController extends BaseController {
 				}else if ("311".equals(workProjectNotify.getType())) {//意见反馈
 					String url = getFeedback(workProjectNotify,model);
 					return url;
+				} else if ("201".equals(workProjectNotify.getType())) {//知识库积分统计通知
+					// notifyId格式: 201,year,month
+					String[] arguments = workProjectNotify.getNotifyId().split(",");
+					Integer year = Integer.valueOf(arguments[0 + 1]); // arguments[1] = year
+					Integer month = Integer.valueOf(arguments[0 + 2]); // arguments[2] = month
+
+					// 查询看板数据(公司级+部门级)
+					List<WkPointSnapshotSummary> dashboardData = wkPointSnapshotService.getDashboardData(year, month);
+					model.addAttribute("dashboardData", dashboardData);
+					model.addAttribute("year", year);
+					model.addAttribute("month", month);
+
+					if (workProjectNotify.getRemarks().contains("待通知") || "view".equals(workProjectNotify.getView())) {
+						return "modules/WorkKnowledgeBase/workKnowledgeBasePointDashboard";
+					}
 				}
 			}
 		}

+ 246 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WkPointSnapshotDetailDao.xml

@@ -0,0 +1,246 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeeplus.modules.WorkKnowledgeBase.dao.WkPointSnapshotDetailDao">
+
+    <sql id="snapshotDetailColumns">
+        a.id AS "id",
+        a.del_flag AS "delFlag",
+        a.create_date AS "createDate",
+        a.snapshot_year AS "snapshotYear",
+        a.snapshot_month AS "snapshotMonth",
+        a.office_id AS "officeId",
+        a.office_name AS "officeName",
+        a.user_id AS "userId",
+        a.user_name AS "userName",
+        a.share_id AS "shareId",
+        a.share_name AS "shareName",
+        a.tree_node_id AS "treeNodeId",
+        a.share_del_flag AS "shareDelFlag",
+        a.category_id AS "categoryId",
+        a.category_name AS "categoryName",
+        a.rule_id AS "ruleId",
+        a.rule_type AS "ruleType",
+        a.point AS "point",
+        a.point_create_date AS "pointCreateDate"
+    </sql>
+
+    <!-- 从 point_detail 实时表查询指定日期范围内的所有积分明细(快照生成用) -->
+    <select id="findAllPointDetails" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotDetail">
+        SELECT
+            d.user_id     AS "userId",
+            IFNULL(u.name, '')        AS "userName",
+            IFNULL(u.office_id, '')   AS "officeId",
+            IFNULL(o.name, '')        AS "officeName",
+            IFNULL(d.share_id, '')    AS "shareId",
+            d.category_id AS "categoryId",
+            d.rule_id     AS "ruleId",
+            CAST(IFNULL(d.point, 0) AS SIGNED) AS "point",
+            d.create_date AS "pointCreateDate",
+            IFNULL(s.name, '')          AS "shareName",
+            IFNULL(s.tree_node_id, '')  AS "treeNodeId",
+            IFNULL(s.del_flag, '1')     AS "shareDelFlag",
+            IFNULL(c.category_name, '') AS "categoryName",
+            CAST(IFNULL(r.rule_type, 0) AS SIGNED) AS "ruleType"
+        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'
+        LEFT JOIN work_knowledge_base_share_info s ON s.id = d.share_id
+        LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+        LEFT JOIN work_knowledge_base_point_rule r ON r.id = d.rule_id AND r.del_flag = '0'
+        WHERE d.del_flag = '0'
+          AND d.create_date &gt;= #{beginDate}
+          AND d.create_date &lt;= TIMESTAMP(#{endDate}, '23:59:59')
+        ORDER BY d.create_date DESC
+    </select>
+
+    <!-- 钻取查询:从快照明细表查询 -->
+    <select id="findDrillDownList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotDetail">
+        SELECT <include refid="snapshotDetailColumns"/>
+        FROM wk_point_snapshot_detail a
+        <where>
+            a.del_flag = '0'
+            AND a.snapshot_year = #{snapshotYear}
+            AND a.snapshot_month = #{snapshotMonth}
+            <!-- 用户筛选 -->
+            <if test="userId != null and userId != ''">
+                AND a.user_id = #{userId}
+            </if>
+            <!-- 部门筛选(包含子部门) -->
+            <if test="officeId != null and officeId != ''">
+                <choose>
+                    <when test="officeId == '一部本部'">
+                        AND a.office_id IN (
+                            SELECT so.id FROM sys_office so
+                            WHERE so.del_flag = '0'
+                            AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                        )
+                    </when>
+                    <otherwise>
+                        AND (a.office_id = #{officeId} OR a.office_id IN (
+                            SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                        ))
+                    </otherwise>
+                </choose>
+            </if>
+            <!-- L2分类筛选 -->
+            <if test="categoryId != null and categoryId != ''">
+                AND a.category_id = #{categoryId}
+            </if>
+            <!-- 规则类型筛选 -->
+            <if test="ruleType != null">
+                AND a.rule_type = #{ruleType}
+            </if>
+            <!-- 积分符号过滤 -->
+            <choose>
+                <when test="columnId != null and columnId == 'deduction'">
+                    AND a.point &lt; 0
+                </when>
+                <otherwise>
+                    AND a.point &gt; 0
+                </otherwise>
+            </choose>
+            <!-- 文档名称模糊查询 -->
+            <if test="shareNameQuery != null and shareNameQuery != ''">
+                AND a.share_name LIKE CONCAT('%', #{shareNameQuery}, '%')
+            </if>
+            <!-- 人员姓名模糊查询 -->
+            <if test="userNameQuery != null and userNameQuery != ''">
+                AND a.user_name LIKE CONCAT('%', #{userNameQuery}, '%')
+            </if>
+            <!-- 部门名称模糊查询 -->
+            <if test="officeNameQuery != null and officeNameQuery != ''">
+                AND a.office_name LIKE CONCAT('%', #{officeNameQuery}, '%')
+            </if>
+        </where>
+        ORDER BY a.point_create_date DESC
+    </select>
+
+    <select id="findDrillDownCount" resultType="java.lang.Integer">
+        SELECT COUNT(1)
+        FROM wk_point_snapshot_detail a
+        <where>
+            a.del_flag = '0'
+            AND a.snapshot_year = #{snapshotYear}
+            AND a.snapshot_month = #{snapshotMonth}
+            <if test="userId != null and userId != ''">
+                AND a.user_id = #{userId}
+            </if>
+            <if test="officeId != null and officeId != ''">
+                <choose>
+                    <when test="officeId == '一部本部'">
+                        AND a.office_id IN (
+                            SELECT so.id FROM sys_office so
+                            WHERE so.del_flag = '0'
+                            AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                        )
+                    </when>
+                    <otherwise>
+                        AND (a.office_id = #{officeId} OR a.office_id IN (
+                            SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                        ))
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="categoryId != null and categoryId != ''">
+                AND a.category_id = #{categoryId}
+            </if>
+            <if test="ruleType != null">
+                AND a.rule_type = #{ruleType}
+            </if>
+            <choose>
+                <when test="columnId != null and columnId == 'deduction'">
+                    AND a.point &lt; 0
+                </when>
+                <otherwise>
+                    AND a.point &gt; 0
+                </otherwise>
+            </choose>
+            <!-- 文档名称模糊查询 -->
+            <if test="shareNameQuery != null and shareNameQuery != ''">
+                AND a.share_name LIKE CONCAT('%', #{shareNameQuery}, '%')
+            </if>
+            <!-- 人员姓名模糊查询 -->
+            <if test="userNameQuery != null and userNameQuery != ''">
+                AND a.user_name LIKE CONCAT('%', #{userNameQuery}, '%')
+            </if>
+            <!-- 部门名称模糊查询 -->
+            <if test="officeNameQuery != null and officeNameQuery != ''">
+                AND a.office_name LIKE CONCAT('%', #{officeNameQuery}, '%')
+            </if>
+        </where>
+    </select>
+
+    <insert id="insert">
+        INSERT INTO wk_point_snapshot_detail (
+            id, del_flag, create_date,
+            snapshot_year, snapshot_month,
+            office_id, office_name, user_id, user_name,
+            share_id, share_name, tree_node_id, share_del_flag,
+            category_id, category_name, rule_id, rule_type,
+            point, point_create_date
+        ) VALUES (
+            #{id}, #{delFlag}, #{createDate},
+            #{snapshotYear}, #{snapshotMonth},
+            #{officeId}, #{officeName}, #{userId}, #{userName},
+            #{shareId}, #{shareName}, #{treeNodeId}, #{shareDelFlag},
+            #{categoryId}, #{categoryName}, #{ruleId}, #{ruleType},
+            #{point}, #{pointCreateDate}
+        )
+    </insert>
+
+    <insert id="batchInsert">
+        INSERT INTO wk_point_snapshot_detail (
+            id, del_flag, create_date,
+            snapshot_year, snapshot_month,
+            office_id, office_name, user_id, user_name,
+            share_id, share_name, tree_node_id, share_del_flag,
+            category_id, category_name, rule_id, rule_type,
+            point, point_create_date
+        ) VALUES
+        <foreach collection="list" item="item" separator=",">
+            (#{item.id}, '0', NOW(),
+             #{item.snapshotYear}, #{item.snapshotMonth},
+             #{item.officeId}, #{item.officeName}, #{item.userId}, #{item.userName},
+             #{item.shareId}, #{item.shareName}, #{item.treeNodeId}, #{item.shareDelFlag},
+             #{item.categoryId}, #{item.categoryName}, #{item.ruleId}, #{item.ruleType},
+             #{item.point}, #{item.pointCreateDate})
+        </foreach>
+    </insert>
+
+    <update id="update">
+        UPDATE wk_point_snapshot_detail SET
+            share_name = #{shareName},
+            category_name = #{categoryName},
+            point = #{point}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="delete">
+        DELETE FROM wk_point_snapshot_detail WHERE id = #{id}
+    </delete>
+
+    <update id="deleteByYearMonth">
+        DELETE FROM wk_point_snapshot_detail
+        WHERE snapshot_year = #{snapshotYear} AND snapshot_month = #{snapshotMonth}
+    </update>
+
+    <select id="get" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotDetail">
+        SELECT <include refid="snapshotDetailColumns"/>
+        FROM wk_point_snapshot_detail a
+        WHERE a.id = #{id} AND a.del_flag = '0'
+    </select>
+
+    <select id="findList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotDetail">
+        SELECT <include refid="snapshotDetailColumns"/>
+        FROM wk_point_snapshot_detail a
+        WHERE a.del_flag = '0'
+        <if test="snapshotYear != null">
+            AND a.snapshot_year = #{snapshotYear}
+        </if>
+        <if test="snapshotMonth != null">
+            AND a.snapshot_month = #{snapshotMonth}
+        </if>
+        ORDER BY a.point_create_date DESC
+    </select>
+
+</mapper>

+ 159 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WkPointSnapshotInfoDao.xml

@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeeplus.modules.WorkKnowledgeBase.dao.WkPointSnapshotInfoDao">
+
+    <sql id="snapshotInfoColumns">
+        a.id AS "id",
+        a.del_flag AS "delFlag",
+        a.create_date AS "createDate",
+        a.snapshot_year AS "snapshotYear",
+        a.snapshot_month AS "snapshotMonth",
+        a.office_id AS "officeId",
+        a.office_name AS "officeName",
+        a.user_id AS "userId",
+        a.user_name AS "userName",
+        a.upload_count AS "uploadCount",
+        a.field1 AS "field1",
+        a.field2 AS "field2",
+        a.field3 AS "field3",
+        a.field4 AS "field4",
+        a.field5 AS "field5",
+        a.field6 AS "field6",
+        a.field7 AS "field7",
+        a.field8 AS "field8",
+        a.field9 AS "field9",
+        a.field10 AS "field10",
+        a.field11 AS "field11",
+        a.field12 AS "field12",
+        a.field13 AS "field13",
+        a.field14 AS "field14",
+        a.field15 AS "field15",
+        a.field16 AS "field16",
+        a.field17 AS "field17",
+        a.field18 AS "field18",
+        a.field19 AS "field19",
+        a.field20 AS "field20"
+    </sql>
+
+    <select id="get" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo">
+        SELECT <include refid="snapshotInfoColumns"/>
+        FROM wk_point_snapshot_info a
+        WHERE a.id = #{id} AND a.del_flag = '0'
+    </select>
+
+    <select id="findList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo">
+        SELECT <include refid="snapshotInfoColumns"/>
+        FROM wk_point_snapshot_info a
+        WHERE a.del_flag = '0'
+        <if test="snapshotYear != null">
+            AND a.snapshot_year = #{snapshotYear}
+        </if>
+        <if test="snapshotMonth != null">
+            AND a.snapshot_month = #{snapshotMonth}
+        </if>
+        <if test="officeId != null and officeId != ''">
+            <choose>
+                <when test="officeId == '一部本部'">
+                    AND a.office_id IN (
+                        SELECT so.id FROM sys_office so
+                        WHERE so.del_flag = '0'
+                        AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                    )
+                </when>
+                <otherwise>
+                    AND (a.office_id = #{officeId} OR a.office_id IN (
+                        SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                    ))
+                </otherwise>
+            </choose>
+        </if>
+        <if test="userId != null and userId != ''">
+            AND a.user_id = #{userId}
+        </if>
+        ORDER BY a.office_name, a.user_name
+    </select>
+
+    <select id="findByYearMonth" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo">
+        SELECT <include refid="snapshotInfoColumns"/>
+        FROM wk_point_snapshot_info a
+        WHERE a.del_flag = '0'
+          AND a.snapshot_year = #{snapshotYear}
+          AND a.snapshot_month = #{snapshotMonth}
+        ORDER BY a.office_name, a.user_name
+    </select>
+
+    <select id="findByYearMonthAndOffice" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo">
+        SELECT <include refid="snapshotInfoColumns"/>
+        FROM wk_point_snapshot_info a
+        WHERE a.del_flag = '0'
+          AND a.snapshot_year = #{snapshotYear}
+          AND a.snapshot_month = #{snapshotMonth}
+        <if test="officeId != null and officeId != ''">
+            <choose>
+                <when test="officeId == '一部本部'">
+                    AND a.office_id IN (
+                        SELECT so.id FROM sys_office so
+                        WHERE so.del_flag = '0'
+                        AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                    )
+                </when>
+                <otherwise>
+                    AND (a.office_id = #{officeId} OR a.office_id IN (
+                        SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                    ))
+                </otherwise>
+            </choose>
+        </if>
+        <if test="userId != null and userId != ''">
+          AND a.user_id = #{userId}
+        </if>
+        <if test="(userId == null or userId == '') and userName != null and userName != ''">
+          AND a.user_name LIKE CONCAT('%', #{userName}, '%')
+        </if>
+        ORDER BY a.user_name
+    </select>
+
+    <select id="findByYearMonthAndUser" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotInfo">
+        SELECT <include refid="snapshotInfoColumns"/>
+        FROM wk_point_snapshot_info a
+        WHERE a.del_flag = '0'
+          AND a.snapshot_year = #{snapshotYear}
+          AND a.snapshot_month = #{snapshotMonth}
+          AND a.user_id = #{userId}
+        LIMIT 1
+    </select>
+
+    <insert id="insert">
+        INSERT INTO wk_point_snapshot_info (
+            id, del_flag, create_date,
+            snapshot_year, snapshot_month, office_id, office_name, user_id, user_name, upload_count,
+            field1, field2, field3, field4, field5, field6, field7, field8, field9, field10,
+            field11, field12, field13, field14, field15, field16, field17, field18, field19, field20
+        ) VALUES (
+            #{id}, #{delFlag}, #{createDate},
+            #{snapshotYear}, #{snapshotMonth}, #{officeId}, #{officeName}, #{userId}, #{userName}, #{uploadCount},
+            #{field1}, #{field2}, #{field3}, #{field4}, #{field5}, #{field6}, #{field7}, #{field8}, #{field9}, #{field10},
+            #{field11}, #{field12}, #{field13}, #{field14}, #{field15}, #{field16}, #{field17}, #{field18}, #{field19}, #{field20}
+        )
+    </insert>
+
+    <update id="update">
+        UPDATE wk_point_snapshot_info SET
+            upload_count = #{uploadCount},
+            field1 = #{field1}, field2 = #{field2}, field3 = #{field3}, field4 = #{field4}, field5 = #{field5},
+            field6 = #{field6}, field7 = #{field7}, field8 = #{field8}, field9 = #{field9}, field10 = #{field10},
+            field11 = #{field11}, field12 = #{field12}, field13 = #{field13}, field14 = #{field14}, field15 = #{field15},
+            field16 = #{field16}, field17 = #{field17}, field18 = #{field18}, field19 = #{field19}, field20 = #{field20}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="delete">
+        DELETE FROM wk_point_snapshot_info WHERE id = #{id}
+    </delete>
+
+    <update id="deleteByYearMonth">
+        DELETE FROM wk_point_snapshot_info
+        WHERE snapshot_year = #{snapshotYear} AND snapshot_month = #{snapshotMonth}
+    </update>
+
+</mapper>

+ 151 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WkPointSnapshotSummaryDao.xml

@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeeplus.modules.WorkKnowledgeBase.dao.WkPointSnapshotSummaryDao">
+
+    <sql id="snapshotSummaryColumns">
+        a.id AS "id",
+        a.del_flag AS "delFlag",
+        a.create_date AS "createDate",
+        a.snapshot_year AS "snapshotYear",
+        a.snapshot_month AS "snapshotMonth",
+        a.grade_type AS "gradeType",
+        a.office_id AS "officeId",
+        a.office_name AS "officeName",
+        a.user_id AS "userId",
+        a.user_name AS "userName",
+        a.upload_count AS "uploadCount",
+        a.basic_subtotal AS "basicSubtotal",
+        a.basic_exchange AS "basicExchange",
+        a.basic_remaining AS "basicRemaining",
+        a.contrib_subtotal AS "contribSubtotal",
+        a.contrib_deduction AS "contribDeduction",
+        a.contrib_exchange AS "contribExchange",
+        a.contrib_remaining AS "contribRemaining"
+    </sql>
+
+    <select id="get" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary">
+        SELECT <include refid="snapshotSummaryColumns"/>
+        FROM wk_point_snapshot_summary a
+        WHERE a.id = #{id} AND a.del_flag = '0'
+    </select>
+
+    <select id="findList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary">
+        SELECT <include refid="snapshotSummaryColumns"/>
+        FROM wk_point_snapshot_summary a
+        WHERE a.del_flag = '0'
+        <if test="snapshotYear != null">
+            AND a.snapshot_year = #{snapshotYear}
+        </if>
+        <if test="snapshotMonth != null">
+            AND a.snapshot_month = #{snapshotMonth}
+        </if>
+        <if test="gradeType != null and gradeType != ''">
+            AND a.grade_type = #{gradeType}
+        </if>
+        <if test="officeId != null and officeId != ''">
+            <choose>
+                <when test="officeId == '一部本部'">
+                    AND a.office_id IN (
+                        SELECT so.id FROM sys_office so
+                        WHERE so.del_flag = '0'
+                        AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                    )
+                </when>
+                <otherwise>
+                    AND (a.office_id = #{officeId} OR a.office_id IN (
+                        SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                    ))
+                </otherwise>
+            </choose>
+        </if>
+        ORDER BY
+            CASE a.grade_type WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3 END,
+            a.office_name, a.user_name
+    </select>
+
+    <select id="findByYearMonth" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary">
+        SELECT <include refid="snapshotSummaryColumns"/>
+        FROM wk_point_snapshot_summary a
+        WHERE a.del_flag = '0'
+          AND a.snapshot_year = #{snapshotYear}
+          AND a.snapshot_month = #{snapshotMonth}
+        ORDER BY
+            CASE a.grade_type WHEN '1' THEN 1 WHEN '2' THEN 2 WHEN '3' THEN 3 END,
+            a.office_name, a.user_name
+    </select>
+
+    <select id="findByYearMonthAndGrade" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary">
+        SELECT <include refid="snapshotSummaryColumns"/>
+        FROM wk_point_snapshot_summary a
+        WHERE a.del_flag = '0'
+          AND a.snapshot_year = #{snapshotYear}
+          AND a.snapshot_month = #{snapshotMonth}
+          AND a.grade_type = #{gradeType}
+        ORDER BY a.office_name, a.user_name
+    </select>
+
+    <select id="findEmployeeByYearMonthAndOffice" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WkPointSnapshotSummary">
+        SELECT <include refid="snapshotSummaryColumns"/>
+        FROM wk_point_snapshot_summary a
+        WHERE a.del_flag = '0'
+          AND a.snapshot_year = #{snapshotYear}
+          AND a.snapshot_month = #{snapshotMonth}
+          AND a.grade_type = '3'
+        <if test="officeId != null and officeId != ''">
+            <choose>
+                <when test="officeId == '一部本部'">
+                    AND a.office_id IN (
+                        SELECT so.id FROM sys_office so
+                        WHERE so.del_flag = '0'
+                        AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                    )
+                </when>
+                <otherwise>
+                    AND (a.office_id = #{officeId} OR a.office_id IN (
+                        SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                    ))
+                </otherwise>
+            </choose>
+        </if>
+        <if test="userId != null and userId != ''">
+          AND a.user_id = #{userId}
+        </if>
+        <if test="(userId == null or userId == '') and userName != null and userName != ''">
+          AND a.user_name LIKE CONCAT('%', #{userName}, '%')
+        </if>
+        ORDER BY a.user_name
+    </select>
+
+    <insert id="insert">
+        INSERT INTO wk_point_snapshot_summary (
+            id, del_flag, create_date,
+            snapshot_year, snapshot_month, grade_type, office_id, office_name, user_id, user_name, upload_count,
+            basic_subtotal, basic_exchange, basic_remaining,
+            contrib_subtotal, contrib_deduction, contrib_exchange, contrib_remaining
+        ) VALUES (
+            #{id}, #{delFlag}, #{createDate},
+            #{snapshotYear}, #{snapshotMonth}, #{gradeType}, #{officeId}, #{officeName}, #{userId}, #{userName}, #{uploadCount},
+            #{basicSubtotal}, #{basicExchange}, #{basicRemaining},
+            #{contribSubtotal}, #{contribDeduction}, #{contribExchange}, #{contribRemaining}
+        )
+    </insert>
+
+    <update id="update">
+        UPDATE wk_point_snapshot_summary SET
+            upload_count = #{uploadCount},
+            basic_subtotal = #{basicSubtotal}, basic_exchange = #{basicExchange}, basic_remaining = #{basicRemaining},
+            contrib_subtotal = #{contribSubtotal}, contrib_deduction = #{contribDeduction},
+            contrib_exchange = #{contribExchange}, contrib_remaining = #{contribRemaining}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="delete">
+        DELETE FROM wk_point_snapshot_summary WHERE id = #{id}
+    </delete>
+
+    <update id="deleteByYearMonth">
+        DELETE FROM wk_point_snapshot_summary
+        WHERE snapshot_year = #{snapshotYear} AND snapshot_month = #{snapshotMonth}
+    </update>
+
+</mapper>

+ 14 - 1
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDao.xml

@@ -44,7 +44,20 @@
                 AND u.name LIKE CONCAT('%', #{userName}, '%')
             </if>
             <if test="officeId != null and officeId != ''">
-                AND u.office_id = #{officeId}
+                <choose>
+                    <when test="officeId == '一部本部'">
+                        AND u.office_id IN (
+                            SELECT so.id FROM sys_office so
+                            WHERE so.del_flag = '0'
+                            AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                        )
+                    </when>
+                    <otherwise>
+                        AND (u.office_id = #{officeId} OR u.office_id IN (
+                            SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                        ))
+                    </otherwise>
+                </choose>
             </if>
             <if test="changeType != null">
                 AND a.change_type = #{changeType}

+ 129 - 3
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml

@@ -443,6 +443,7 @@
         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(uc.upload_count, 0) AS "uploadCount",
             COALESCE(cp.create_point, 0) AS "createPoint",
@@ -551,7 +552,7 @@
         LEFT JOIN (
             SELECT user_id, SUM(ABS(point)) AS exchange_point
             FROM work_knowledge_base_point_detail
-            WHERE del_flag = '0' AND point &lt; 0
+            WHERE del_flag = '0' AND point &lt; 0 AND category_id != '10'
             <if test="beginDate != null and beginDate != ''">
                 AND create_date &gt;= #{beginDate}
             </if>
@@ -716,7 +717,7 @@
         GROUP BY d.user_id, p.tree_name
     </select>
 
-    <!-- 按rule_id分组统计积分总和(用于三层表头导出) -->
+    <!-- 按rule_id分组统计积分总和(用于三层表头导出,包含负数扣分) -->
     <select id="sumPointsByRuleGrouped" resultType="java.util.HashMap">
         SELECT
             d.user_id AS userId,
@@ -727,7 +728,6 @@
         FROM work_knowledge_base_point_detail d
         INNER JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
         WHERE d.del_flag = '0'
-          AND d.point &gt; 0
           AND d.rule_id IS NOT NULL AND d.rule_id != ''
           AND d.user_id IN
           <foreach collection="userIds" item="userId" open="(" separator="," close=")">
@@ -755,6 +755,7 @@
         INNER JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
         WHERE d.del_flag = '0'
           AND d.point &lt; 0
+          AND d.category_id != '10'
           AND (
               -- 情况1:category本身是L1分类(parent_id='0')
               c.parent_id = '0'
@@ -779,4 +780,129 @@
             END
     </select>
 
+    <!-- 钻取查询:按部门+L2分类+规则类型查询积分明细文件列表 -->
+    <select id="findDrillDownList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.UserPointDetail">
+        SELECT
+            d.id          AS "id",
+            d.user_id     AS "userId",
+            IFNULL(u.name, '')        AS "userName",
+            IFNULL(o.name, '')        AS "officeName",
+            IFNULL(d.share_id, '')    AS "shareId",
+            d.category_id AS "categoryId",
+            d.rule_id     AS "ruleId",
+            CAST(IFNULL(d.point, 0) AS SIGNED) AS "point",
+            d.create_date AS "createDate",
+            IFNULL(s.name, '')          AS "shareName",
+            IFNULL(s.tree_node_id, '')  AS "treeNodeId",
+            IFNULL(s.del_flag, '1')     AS "shareDelFlag",
+            IFNULL(c.category_name, '') AS "categoryName",
+            CAST(IFNULL(r.rule_type, 0) AS SIGNED) AS "ruleType"
+        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'
+        LEFT JOIN work_knowledge_base_share_info s ON s.id = d.share_id
+        LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+        LEFT JOIN work_knowledge_base_point_rule r ON r.id = d.rule_id AND r.del_flag = '0'
+        <where>
+            d.del_flag = '0'
+            <!-- 用户筛选(员工级钻取) -->
+            <if test="userId != null and userId != ''">
+                AND d.user_id = #{userId}
+            </if>
+            <!-- 部门筛选:包含子部门 -->
+            <if test="officeId != null and officeId != ''">
+                <choose>
+                    <when test="officeId == '一部本部'">
+                        AND u.office_id IN (
+                            SELECT so.id FROM sys_office so
+                            WHERE so.del_flag = '0'
+                            AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                        )
+                    </when>
+                    <otherwise>
+                        AND (u.office_id = #{officeId} OR u.office_id IN (
+                            SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                        ))
+                    </otherwise>
+                </choose>
+            </if>
+            <!-- L2分类筛选 -->
+            <if test="categoryId != null and categoryId != ''">
+                AND d.category_id = #{categoryId}
+            </if>
+            <!-- 规则类型筛选:1=创建, 2=审核, 10=阅读, 20=点赞 -->
+            <if test="ruleType != null">
+                AND r.rule_type = #{ruleType}
+            </if>
+            <!-- 日期范围 -->
+            <if test="beginDate != null">
+                AND d.create_date &gt;= #{beginDate}
+            </if>
+            <if test="endDate != null">
+                AND d.create_date &lt;= TIMESTAMP(#{endDate}, '23:59:59')
+            </if>
+            <!-- 积分符号过滤:deduction=负积分,其他=正积分 -->
+            <choose>
+                <when test="columnId != null and columnId == 'deduction'">
+                    AND d.point &lt; 0
+                </when>
+                <otherwise>
+                    AND d.point &gt; 0
+                </otherwise>
+            </choose>
+        </where>
+        ORDER BY d.create_date DESC
+    </select>
+
+    <select id="findDrillDownCount" resultType="java.lang.Integer">
+        SELECT COUNT(1)
+        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 work_knowledge_base_share_info s ON s.id = d.share_id
+        LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+        LEFT JOIN work_knowledge_base_point_rule r ON r.id = d.rule_id AND r.del_flag = '0'
+        <where>
+            d.del_flag = '0'
+            <if test="userId != null and userId != ''">
+                AND d.user_id = #{userId}
+            </if>
+            <if test="officeId != null and officeId != ''">
+                <choose>
+                    <when test="officeId == '一部本部'">
+                        AND u.office_id IN (
+                            SELECT so.id FROM sys_office so
+                            WHERE so.del_flag = '0'
+                            AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                        )
+                    </when>
+                    <otherwise>
+                        AND (u.office_id = #{officeId} OR u.office_id IN (
+                            SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{officeId}, ',%') AND so.del_flag = '0'
+                        ))
+                    </otherwise>
+                </choose>
+            </if>
+            <if test="categoryId != null and categoryId != ''">
+                AND d.category_id = #{categoryId}
+            </if>
+            <if test="ruleType != null">
+                AND r.rule_type = #{ruleType}
+            </if>
+            <if test="beginDate != null">
+                AND d.create_date &gt;= #{beginDate}
+            </if>
+            <if test="endDate != null">
+                AND d.create_date &lt;= TIMESTAMP(#{endDate}, '23:59:59')
+            </if>
+            <choose>
+                <when test="columnId != null and columnId == 'deduction'">
+                    AND d.point &lt; 0
+                </when>
+                <otherwise>
+                    AND d.point &gt; 0
+                </otherwise>
+            </choose>
+        </where>
+    </select>
+
 </mapper>

+ 42 - 8
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseShareInfoDao.xml

@@ -164,11 +164,28 @@
                       AND u.del_flag = '0'
                 )
             </if>
-            <!-- 所属人部门筛选(支持ID精确匹配或部门名称模糊查询) -->
+            <!-- 所属人部门筛选(包含子部门,支持ID精确匹配或部门名称模糊查询) -->
             <if test="entity.ownerOfficeId != null and entity.ownerOfficeId != ''">
-                AND COALESCE(a.submit_audit_user_id, a.create_by) IN (
-                    SELECT u.id FROM sys_user u WHERE u.office_id = #{entity.ownerOfficeId} AND u.del_flag = '0'
-                )
+                <choose>
+                    <when test="entity.ownerOfficeId == '一部本部'">
+                        AND COALESCE(a.submit_audit_user_id, a.create_by) IN (
+                            SELECT u.id FROM sys_user u WHERE u.del_flag = '0'
+                            AND u.office_id IN (
+                                SELECT so.id FROM sys_office so
+                                WHERE so.del_flag = '0'
+                                AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                            )
+                        )
+                    </when>
+                    <otherwise>
+                        AND COALESCE(a.submit_audit_user_id, a.create_by) IN (
+                            SELECT u.id FROM sys_user u WHERE u.del_flag = '0'
+                            AND (u.office_id = #{entity.ownerOfficeId} OR u.office_id IN (
+                                SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{entity.ownerOfficeId}, ',%') AND so.del_flag = '0'
+                            ))
+                        )
+                    </otherwise>
+                </choose>
             </if>
             <if test="entity.ownerOfficeName != null and entity.ownerOfficeName != ''">
                 AND EXISTS (
@@ -266,11 +283,28 @@
                       AND u.del_flag = '0'
                 )
             </if>
-            <!-- 所属人部门筛选(支持ID精确匹配或部门名称模糊查询) -->
+            <!-- 所属人部门筛选(包含子部门,支持ID精确匹配或部门名称模糊查询) -->
             <if test="entity.ownerOfficeId != null and entity.ownerOfficeId != ''">
-                AND COALESCE(a.submit_audit_user_id, a.create_by) IN (
-                    SELECT u.id FROM sys_user u WHERE u.office_id = #{entity.ownerOfficeId} AND u.del_flag = '0'
-                )
+                <choose>
+                    <when test="entity.ownerOfficeId == '一部本部'">
+                        AND COALESCE(a.submit_audit_user_id, a.create_by) IN (
+                            SELECT u.id FROM sys_user u WHERE u.del_flag = '0'
+                            AND u.office_id IN (
+                                SELECT so.id FROM sys_office so
+                                WHERE so.del_flag = '0'
+                                AND (so.name = '工程一部' OR so.parent_ids LIKE CONCAT('%', (SELECT so2.id FROM sys_office so2 WHERE so2.name = '工程一部' AND so2.del_flag = '0' LIMIT 1), ',%'))
+                            )
+                        )
+                    </when>
+                    <otherwise>
+                        AND COALESCE(a.submit_audit_user_id, a.create_by) IN (
+                            SELECT u.id FROM sys_user u WHERE u.del_flag = '0'
+                            AND (u.office_id = #{entity.ownerOfficeId} OR u.office_id IN (
+                                SELECT so.id FROM sys_office so WHERE so.parent_ids LIKE CONCAT('%', #{entity.ownerOfficeId}, ',%') AND so.del_flag = '0'
+                            ))
+                        )
+                    </otherwise>
+                </choose>
             </if>
             <if test="entity.ownerOfficeName != null and entity.ownerOfficeName != ''">
                 AND EXISTS (

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

@@ -59,7 +59,7 @@
         WHERE a.del_flag = 0
           AND a.parent_id = '0'
           AND a.status = '1'
-          AND a.id != '9'
+          AND a.id NOT IN ('9', '10')
         ORDER BY a.sort ASC, a.create_date ASC
     </select>
 

+ 49 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgePointNotifyUserDao.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgePointNotifyUserDao">
+
+    <sql id="notifyUserColumns">
+        a.id AS "id",
+        a.user_id AS "userId",
+        a.del_flag AS "delFlag",
+        a.create_date AS "createDate"
+    </sql>
+
+    <select id="get" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgePointNotifyUser">
+        SELECT <include refid="notifyUserColumns"/>
+        FROM work_knowledge_point_notify_user a
+        WHERE a.id = #{id} AND a.del_flag = '0'
+    </select>
+
+    <select id="findList" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgePointNotifyUser">
+        SELECT <include refid="notifyUserColumns"/>
+        FROM work_knowledge_point_notify_user a
+        WHERE a.del_flag = '0'
+        ORDER BY a.create_date
+    </select>
+
+    <select id="findAllNotifyUsers" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgePointNotifyUser">
+        SELECT <include refid="notifyUserColumns"/>
+        FROM work_knowledge_point_notify_user a
+        WHERE a.del_flag = '0'
+    </select>
+
+    <insert id="insert">
+        INSERT INTO work_knowledge_point_notify_user (
+            id, user_id, del_flag, create_date
+        ) VALUES (
+            #{id}, #{userId}, #{delFlag}, #{createDate}
+        )
+    </insert>
+
+    <update id="update">
+        UPDATE work_knowledge_point_notify_user SET
+            user_id = #{userId}
+        WHERE id = #{id}
+    </update>
+
+    <delete id="delete">
+        DELETE FROM work_knowledge_point_notify_user WHERE id = #{id}
+    </delete>
+
+</mapper>

+ 104 - 0
src/main/resources/mysqlDateBase/wk_point_snapshot_tables.sql

@@ -0,0 +1,104 @@
+-- =============================================
+-- 知识库积分看板功能 - 建表SQL
+-- =============================================
+
+-- 1. 知识库积分通知人员表
+CREATE TABLE `work_knowledge_point_notify_user` (
+    `id` VARCHAR(64) NOT NULL COMMENT '主键',
+    `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID(关联sys_user.id)',
+    `del_flag` VARCHAR(1) NOT NULL DEFAULT '0' COMMENT '删除标记(0正常 1删除)',
+    `create_date` DATETIME DEFAULT NULL COMMENT '创建时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库积分通知人员表';
+
+-- 2. 知识库积分快照明细表(员工级,field存各规则积分值)
+CREATE TABLE IF NOT EXISTS `wk_point_snapshot_info` (
+    `id` VARCHAR(64) NOT NULL COMMENT '主键',
+    `del_flag` VARCHAR(64) DEFAULT '0' COMMENT '删除标记',
+    `create_date` DATETIME DEFAULT NULL COMMENT '创建时间',
+    `snapshot_year` INT NOT NULL COMMENT '年份',
+    `snapshot_month` INT NOT NULL COMMENT '月份',
+    `office_id` VARCHAR(64) DEFAULT NULL COMMENT '部门ID',
+    `office_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称',
+    `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
+    `user_name` VARCHAR(100) DEFAULT NULL COMMENT '用户姓名',
+    `upload_count` INT DEFAULT 0 COMMENT '上传文档数',
+    `field1` VARCHAR(64) DEFAULT NULL COMMENT '基础-阅读积分',
+    `field2` VARCHAR(64) DEFAULT NULL COMMENT '基础-点赞积分',
+    `field3` VARCHAR(64) DEFAULT NULL COMMENT '贡献-技术总结-创建',
+    `field4` VARCHAR(64) DEFAULT NULL COMMENT '贡献-技术总结-审核',
+    `field5` VARCHAR(64) DEFAULT NULL COMMENT '贡献-培训心得-创建',
+    `field6` VARCHAR(64) DEFAULT NULL COMMENT '贡献-培训心得-审核',
+    `field7` VARCHAR(64) DEFAULT NULL COMMENT '贡献-相关资料-创建',
+    `field8` VARCHAR(64) DEFAULT NULL COMMENT '贡献-相关资料-审核',
+    `field9` VARCHAR(64) DEFAULT NULL COMMENT '贡献-问答答疑-创建',
+    `field10` VARCHAR(64) DEFAULT NULL COMMENT '贡献-问答答疑-回答',
+    `field11` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(原创)-创建',
+    `field12` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(原创)-审核',
+    `field13` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(转载)-创建',
+    `field14` VARCHAR(64) DEFAULT NULL COMMENT '贡献-典型案例(转载)-审核',
+    `field15` VARCHAR(64) DEFAULT NULL COMMENT '贡献-扣除积分',
+    `field16` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段16',
+    `field17` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段17',
+    `field18` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段18',
+    `field19` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段19',
+    `field20` VARCHAR(64) DEFAULT NULL COMMENT '扩展字段20',
+    PRIMARY KEY (`id`),
+    KEY `idx_snapshot_ym` (`snapshot_year`, `snapshot_month`),
+    KEY `idx_snapshot_user` (`user_id`, `snapshot_year`, `snapshot_month`),
+    KEY `idx_snapshot_office` (`office_id`, `snapshot_year`, `snapshot_month`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库积分快照明细表(员工级)';
+
+-- 3. 知识库积分快照汇总表(公司/部门/员工三级冗余,看板直接查此表)
+CREATE TABLE IF NOT EXISTS `wk_point_snapshot_summary` (
+    `id` VARCHAR(64) NOT NULL COMMENT '主键',
+    `del_flag` VARCHAR(64) DEFAULT '0' COMMENT '删除标记',
+    `create_date` DATETIME DEFAULT NULL COMMENT '创建时间',
+    `snapshot_year` INT NOT NULL COMMENT '年份',
+    `snapshot_month` INT NOT NULL COMMENT '月份',
+    `grade_type` VARCHAR(64) NOT NULL COMMENT '层级类型(1:公司 2:部门 3:员工)',
+    `office_id` VARCHAR(64) DEFAULT NULL COMMENT '部门ID(公司级为空)',
+    `office_name` VARCHAR(100) DEFAULT NULL COMMENT '部门名称',
+    `user_id` VARCHAR(64) DEFAULT NULL COMMENT '用户ID(员工级才有)',
+    `user_name` VARCHAR(100) DEFAULT NULL COMMENT '用户姓名',
+    `upload_count` INT DEFAULT 0 COMMENT '上传文档数',
+    `basic_subtotal` INT DEFAULT 0 COMMENT '基础积分汇总',
+    `basic_exchange` INT DEFAULT 0 COMMENT '基础已兑换积分',
+    `basic_remaining` INT DEFAULT 0 COMMENT '基础剩余积分',
+    `contrib_subtotal` INT DEFAULT 0 COMMENT '贡献积分汇总(含扣除)',
+    `contrib_deduction` INT DEFAULT 0 COMMENT '贡献-扣除积分',
+    `contrib_exchange` INT DEFAULT 0 COMMENT '贡献已兑换积分',
+    `contrib_remaining` INT DEFAULT 0 COMMENT '贡献剩余积分',
+    PRIMARY KEY (`id`),
+    KEY `idx_summary_ym_grade` (`snapshot_year`, `snapshot_month`, `grade_type`),
+    KEY `idx_summary_office` (`office_id`, `snapshot_year`, `snapshot_month`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库积分快照汇总表(公司/部门/员工三级)';
+
+-- 4. 知识库积分快照明细表(保存每条积分明细的快照,用于钻取查询,确保与看板数字一致)
+CREATE TABLE IF NOT EXISTS `wk_point_snapshot_detail` (
+    `id` VARCHAR(64) NOT NULL COMMENT '主键',
+    `del_flag` VARCHAR(64) DEFAULT '0' COMMENT '删除标记',
+    `create_date` DATETIME DEFAULT NULL COMMENT '创建时间',
+    `snapshot_year` INT NOT NULL COMMENT '快照年份',
+    `snapshot_month` INT NOT NULL COMMENT '快照月份',
+    `office_id` VARCHAR(64) DEFAULT '' COMMENT '部门ID',
+    `office_name` VARCHAR(100) DEFAULT '' COMMENT '部门名称',
+    `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
+    `user_name` VARCHAR(100) DEFAULT '' COMMENT '用户姓名',
+    `share_id` VARCHAR(64) DEFAULT '' COMMENT '文件ID',
+    `share_name` VARCHAR(500) DEFAULT '' COMMENT '文件名称',
+    `tree_node_id` VARCHAR(64) DEFAULT '' COMMENT '树节点ID',
+    `share_del_flag` VARCHAR(64) DEFAULT '1' COMMENT '文件删除标志(快照时的状态)',
+    `category_id` VARCHAR(64) DEFAULT '' COMMENT '分类ID',
+    `category_name` VARCHAR(200) DEFAULT '' COMMENT '分类名称',
+    `rule_id` VARCHAR(64) DEFAULT '' COMMENT '规则ID',
+    `rule_type` INT DEFAULT 0 COMMENT '规则类型(1=创建,2=审核,10=阅读,20=点赞)',
+    `point` INT DEFAULT 0 COMMENT '积分值',
+    `point_create_date` DATETIME DEFAULT NULL COMMENT '积分产生时间',
+    PRIMARY KEY (`id`),
+    KEY `idx_snap_detail_ym` (`snapshot_year`, `snapshot_month`),
+    KEY `idx_snap_detail_user` (`user_id`, `snapshot_year`, `snapshot_month`),
+    KEY `idx_snap_detail_office` (`office_id`, `snapshot_year`, `snapshot_month`),
+    KEY `idx_snap_detail_category` (`category_id`, `snapshot_year`, `snapshot_month`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库积分快照明细表(钻取查询用)';

+ 407 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointDashboard.jsp

@@ -0,0 +1,407 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ include file="/webpage/include/taglib.jsp"%>
+<html>
+<head>
+    <title>知识库积分统计看板</title>
+    <meta name="decorator" content="default"/>
+    <style>
+        .dashboard-section { margin-bottom: 20px; }
+        .dashboard-section .section-title {
+            font-size: 16px; font-weight: bold; padding: 10px 15px;
+            background: #f0f0f0; border-left: 4px solid #1E9FFF; margin-bottom: 10px;
+        }
+        .data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
+        .data-table th, .data-table td {
+            padding: 6px 10px; border: 1px solid #d2d2d2; text-align: center;
+        }
+        .data-table thead th { background: #f8f8f8; font-weight: bold; }
+        .data-table thead th.l1-header-basic { background: #d9edf7; }
+        .data-table thead th.l1-header-contrib { background: #fcf8e3; }
+        .data-table thead th.l2-header { background: #f0f0f0; }
+        .data-table tbody tr:hover { background: #f5f5f5; }
+        .dept-link { color: #1E9FFF; cursor: pointer; text-decoration: underline; }
+        .dept-link:hover { color: #009688; }
+        .data-negative { color: #FF5722; }
+        .data-positive { color: #1aa094; }
+        .modal-table { width: 100%; border-collapse: collapse; font-size: 13px; }
+        .modal-table th, .modal-table td {
+            padding: 8px 12px; border: 1px solid #e6e6e6; text-align: center;
+        }
+        .modal-table th { background: #f8f8f8; font-weight: bold; }
+        .detail-link { color: #1E9FFF; cursor: pointer; }
+        .detail-link:hover { color: #009688; text-decoration: underline; }
+        .clickable-num { color: #1E9FFF; cursor: pointer; text-decoration: underline; }
+        .clickable-num:hover { color: #009688; }
+        .year-month-bar {
+            padding: 10px 15px; background: #fff; margin-bottom: 15px;
+            border: 1px solid #e6e6e6; border-radius: 4px;
+        }
+        .year-month-bar label { margin-right: 5px; font-weight: normal; }
+        .year-month-bar select {
+            padding: 5px 10px; border: 1px solid #d2d2d2; border-radius: 3px;
+            margin-right: 15px; background: #fff;
+        }
+        .year-month-bar .layui-btn { margin-left: 10px; }
+        .company-row td { font-weight: bold; background: #fafafa; }
+    </style>
+    <script type="text/javascript">
+        var ctx = '${ctx}';
+        var currentYear = '${defaultYear}' || '${year}';
+        var currentMonth = '${defaultMonth}' || '${month}';
+        var l2CategoryIdMap = {};
+
+        // 贡献积分列配置:[fieldKey, categoryName, ruleType]
+        var contribColConfig = [
+            ['field3', '技术总结', 1],
+            ['field4', '技术总结', 2],
+            ['field5', '培训心得', 1],
+            ['field6', '培训心得', 2],
+            ['field7', '相关资料', 1],
+            ['field8', '相关资料', 2],
+            ['field9', '问答答疑', 1],
+            ['field10', '问答答疑', 2],
+            ['field11', '典型案例(原创)', 1],
+            ['field12', '典型案例(原创)', 2],
+            ['field13', '典型案例(转载)', 1],
+            ['field14', '典型案例(转载)', 2]
+        ];
+
+        $(document).ready(function() {
+            // 初始化年月选择器
+            initYearMonthOptions();
+            // 更新日期范围标签
+            updateDateRangeLabel();
+            // 更新标题
+            updateSectionTitles();
+            // 加载数据
+            loadDashboardData();
+
+            // 查询按钮
+            $('#btnQuery').click(function() {
+                currentYear = $('#selYear').val();
+                currentMonth = $('#selMonth').val();
+                updateDateRangeLabel();
+                updateSectionTitles();
+                loadDashboardData();
+            });
+        });
+
+        function updateSectionTitles() {
+            var maxDay = new Date(currentYear, currentMonth, 0).getDate();
+            var dateStr = currentYear + '-' + padZero(currentMonth) + '-01 至 ' + currentYear + '-' + padZero(currentMonth) + '-' + padZero(maxDay);
+            $('#companyTitle').text('公司级积分统计汇总(' + dateStr + ')');
+            $('#deptTitle').text('部门级积分统计汇总(' + dateStr + ')(点击部门名称查看员工明细)');
+        }
+
+        function updateDateRangeLabel() {
+            var maxDay = new Date(currentYear, currentMonth, 0).getDate();
+            var label = currentYear + '-' + padZero(currentMonth) + '-01 至 ' + currentYear + '-' + padZero(currentMonth) + '-' + padZero(maxDay);
+            $('#dateRangeLabel').text(label);
+        }
+
+        /** 导出积分统计(根据当前选择的统计时间周期) */
+        function exportPointStats() {
+            var year = $('#selYear').val();
+            var month = $('#selMonth').val();
+            var maxDay = new Date(year, month, 0).getDate();
+            var beginDate = year + '-' + padZero(month) + '-01';
+            var endDate = year + '-' + padZero(month) + '-' + padZero(maxDay);
+            var iframeName = 'exportPointIframe_' + new Date().getTime();
+            var iframe = $('<iframe>', { name: iframeName, style: 'display:none;', id: iframeName }).appendTo('body');
+            var form = $('<form>', {
+                action: ctx + '/workKnowledgeBase/point/exportPointStats',
+                method: 'post',
+                target: iframeName,
+                style: 'display:none;'
+            }).appendTo('body');
+            form.append('<input type="hidden" name="beginDate" value="' + beginDate + '"/>');
+            form.append('<input type="hidden" name="endDate" value="' + endDate + '"/>');
+            form.submit();
+            setTimeout(function() { form.remove(); iframe.remove(); }, 60000);
+        }
+        function padZero(n) { return n < 10 ? '0' + n : '' + n; }
+
+        function initYearMonthOptions() {
+            var now = new Date();
+            var currentY = now.getFullYear();
+            var currentM = now.getMonth() + 1;
+            var yearHtml = '';
+            for (var y = currentY; y >= currentY - 5; y--) {
+                yearHtml += '<option value="' + y + '"' + (y == currentYear ? ' selected' : '') + '>' + y + '年</option>';
+            }
+            $('#selYear').html(yearHtml);
+            var monthHtml = '';
+            for (var m = 1; m <= 12; m++) {
+                monthHtml += '<option value="' + m + '"' + (m == currentMonth ? ' selected' : '') + '>' + m + '月</option>';
+            }
+            $('#selMonth').html(monthHtml);
+        }
+
+        function loadDashboardData() {
+            $.ajax({
+                url: ctx + '/workKnowledgeBase/point/dashboardData',
+                data: { year: currentYear, month: currentMonth },
+                type: 'GET',
+                dataType: 'json',
+                success: function(res) {
+                    if (res.success) {
+                        l2CategoryIdMap = res.l2CategoryIdMap || {};
+                        renderCompanyData(res.companyData, res.companyContrib);
+                        renderDeptData(res.deptData, res.deptContribMap);
+                    } else {
+                        top.layer.msg(res.message || '查询失败', {icon: 2});
+                    }
+                },
+                error: function() {
+                    top.layer.msg('请求失败', {icon: 2});
+                }
+            });
+        }
+
+        function renderCompanyData(data, companyContrib) {
+            companyContrib = companyContrib || {};
+            var html = '<table class="data-table"><thead>';
+            // 三层表头
+            html += '<tr>';
+            html += '<th rowspan="3">名称</th>';
+            html += '<th rowspan="3">上传文档数</th>';
+            html += '<th colspan="5" class="l1-header-basic">基础积分</th>';
+            html += '<th colspan="16" class="l1-header-contrib">贡献积分</th>';
+            html += '</tr>';
+            html += '<tr>';
+            html += '<th rowspan="2" class="l2-header">阅读</th>';
+            html += '<th rowspan="2" class="l2-header">点赞</th>';
+            html += '<th rowspan="2" class="l2-header">积分汇总</th>';
+            html += '<th rowspan="2" class="l2-header">已兑换积分</th>';
+            html += '<th rowspan="2" class="l2-header">剩余积分</th>';
+            html += '<th colspan="2" class="l2-header">技术总结</th>';
+            html += '<th colspan="2" class="l2-header">培训心得</th>';
+            html += '<th colspan="2" class="l2-header">相关资料</th>';
+            html += '<th colspan="2" class="l2-header">问答答疑</th>';
+            html += '<th colspan="2" class="l2-header">典型案例(原创)</th>';
+            html += '<th colspan="2" class="l2-header">典型案例(转载)</th>';
+            html += '<th rowspan="2" class="l2-header">扣除积分</th>';
+            html += '<th rowspan="2" class="l2-header">积分汇总</th>';
+            html += '<th rowspan="2" class="l2-header">已兑换积分</th>';
+            html += '<th rowspan="2" class="l2-header">剩余积分</th>';
+            html += '</tr>';
+            html += '<tr>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">回答</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '</tr>';
+            html += '</thead><tbody>';
+            if (data && data.length > 0) {
+                var d = data[0];
+                html += '<tr class="company-row">';
+                html += '<td>公司汇总</td>';
+                html += '<td>' + fmtVal(d.uploadCount) + '</td>';
+                // 基础积分:阅读=field1, 点赞=field2, 汇总, 已兑换, 剩余
+                html += '<td><span class="clickable-num" onclick="openDrillDown(\'\',\'阅读\',10)">' + (companyContrib.field1 || 0) + '</span></td>';
+                html += '<td><span class="clickable-num" onclick="openDrillDown(\'\',\'点赞\',20)">' + (companyContrib.field2 || 0) + '</span></td>';
+                html += '<td>' + fmtVal(d.basicSubtotal) + '</td>';
+                html += '<td>' + fmtVal(d.basicExchange) + '</td>';
+                html += '<td>' + fmtVal(d.basicRemaining) + '</td>';
+                // 贡献积分各L2:从companyContrib读取field3-field14实际值(每L2有创建/审核两列)
+                for (var ci = 0; ci < contribColConfig.length; ci++) {
+                    var cfg = contribColConfig[ci];
+                    var val = companyContrib[cfg[0]] || 0;
+                    html += '<td><span class="clickable-num" onclick="openDrillDown(\'\',\'' + cfg[1] + '\',' + cfg[2] + ')">' + val + '</span></td>';
+                }
+                html += '<td class="' + negCls(d.contribDeduction) + '"><span class="clickable-num" onclick="openDrillDown(\'\',\'扣除积分\',0,null,\'deduction\')">' + fmtVal(d.contribDeduction) + '</span></td>';
+                html += '<td>' + fmtVal(d.contribSubtotal) + '</td>';
+                html += '<td>' + fmtVal(d.contribExchange) + '</td>';
+                html += '<td class="' + negCls(d.contribRemaining) + '">' + fmtVal(d.contribRemaining) + '</td>';
+                html += '</tr>';
+            } else {
+                html += '<tr><td colspan="23" style="color:#999;">暂无数据</td></tr>';
+            }
+            html += '</tbody></table>';
+            $('#companySummary').html(html);
+        }
+
+        function fmtVal(v) { return (v || 0); }
+        function negCls(v) { return (v < 0) ? 'data-negative' : ''; }
+        function posCls(v) { return (v > 0) ? 'data-positive' : ''; }
+
+        function renderDeptData(data, deptContribMap) {
+            deptContribMap = deptContribMap || {};
+            var html = '<table class="data-table"><thead>';
+            // 三层表头(与导出一致)
+            html += '<tr>';
+            html += '<th rowspan="3">部门名称</th>';
+            html += '<th rowspan="3">上传文档数量</th>';
+            html += '<th colspan="5" class="l1-header-basic">基础积分</th>';
+            html += '<th colspan="16" class="l1-header-contrib">贡献积分</th>';
+            html += '</tr>';
+            html += '<tr>';
+            html += '<th rowspan="2" class="l2-header">阅读</th>';
+            html += '<th rowspan="2" class="l2-header">点赞</th>';
+            html += '<th rowspan="2" class="l2-header">积分汇总</th>';
+            html += '<th rowspan="2" class="l2-header">已兑换积分</th>';
+            html += '<th rowspan="2" class="l2-header">剩余积分</th>';
+            html += '<th colspan="2" class="l2-header">技术总结</th>';
+            html += '<th colspan="2" class="l2-header">培训心得</th>';
+            html += '<th colspan="2" class="l2-header">相关资料</th>';
+            html += '<th colspan="2" class="l2-header">问答答疑</th>';
+            html += '<th colspan="2" class="l2-header">典型案例(原创)</th>';
+            html += '<th colspan="2" class="l2-header">典型案例(转载)</th>';
+            html += '<th rowspan="2" class="l2-header">扣除积分</th>';
+            html += '<th rowspan="2" class="l2-header">积分汇总</th>';
+            html += '<th rowspan="2" class="l2-header">已兑换积分</th>';
+            html += '<th rowspan="2" class="l2-header">剩余积分</th>';
+            html += '</tr>';
+            html += '<tr>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">回答</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '<th class="l2-header">创建</th><th class="l2-header">审核</th>';
+            html += '</tr>';
+            html += '</thead><tbody>';
+            if (data && data.length > 0) {
+                for (var i = 0; i < data.length; i++) {
+                    var d = data[i];
+                    html += '<tr>';
+                    html += '<td><a class="dept-link" onclick="showDeptEmployees(\'' + d.officeId + '\',\'' + escapeHtml(d.officeName || '') + '\')">' + escapeHtml(d.officeName || '') + '</a></td>';
+                    html += '<td>' + fmtVal(d.uploadCount) + '</td>';
+                    // 基础积分:阅读=field1, 点赞=field2
+                    var deptContrib = deptContribMap[d.officeId] || {};
+                    html += '<td><span class="clickable-num" onclick="openDrillDown(\'' + d.officeId + '\',\'阅读\',10,\'' + escapeHtml(d.officeName || '') + '\')">' + (deptContrib.field1 || 0) + '</span></td>';
+                    html += '<td><span class="clickable-num" onclick="openDrillDown(\'' + d.officeId + '\',\'点赞\',20,\'' + escapeHtml(d.officeName || '') + '\')">' + (deptContrib.field2 || 0) + '</span></td>';
+                    html += '<td class="' + posCls(d.basicSubtotal) + '">' + fmtVal(d.basicSubtotal) + '</td>';
+                    html += '<td>' + fmtVal(d.basicExchange) + '</td>';
+                    html += '<td class="' + posCls(d.basicRemaining) + '">' + fmtVal(d.basicRemaining) + '</td>';
+                    // 贡献积分各L2:从deptContribMap读取该部门的field3-field14实际值
+                    for (var ci = 0; ci < contribColConfig.length; ci++) {
+                        var cfg = contribColConfig[ci];
+                        var val = deptContrib[cfg[0]] || 0;
+                        html += '<td><span class="clickable-num" onclick="openDrillDown(\'' + d.officeId + '\',\'' + cfg[1] + '\',' + cfg[2] + ',\'' + escapeHtml(d.officeName || '') + '\')">' + val + '</span></td>';
+                    }
+                    html += '<td class="' + negCls(d.contribDeduction) + '"><span class="clickable-num" onclick="openDrillDown(\'' + d.officeId + '\',\'扣除积分\',0,\'' + escapeHtml(d.officeName || '') + '\',\'deduction\')">' + fmtVal(d.contribDeduction) + '</span></td>';
+                    html += '<td class="' + posCls(d.contribSubtotal) + '">' + fmtVal(d.contribSubtotal) + '</td>';
+                    html += '<td>' + fmtVal(d.contribExchange) + '</td>';
+                    html += '<td class="' + negCls(d.contribRemaining) + '">' + fmtVal(d.contribRemaining) + '</td>';
+                    html += '</tr>';
+                }
+            } else {
+                html += '<tr><td colspan="23" style="color:#999;">暂无数据</td></tr>';
+            }
+            html += '</tbody></table>';
+            $('#deptTable').html(html);
+        }
+
+        function showDeptEmployees(officeId, officeName) {
+            top.layer.open({
+                type: 2,
+                area: ['95%', '90%'],
+                title: officeName + ' - 员工积分明细(' + currentYear + '年' + currentMonth + '月)',
+                maxmin: true,
+                content: ctx + '/workKnowledgeBase/point/deptEmployeesPage?year=' + currentYear + '&month=' + currentMonth + '&officeId=' + officeId + '&officeName=' + encodeURIComponent(officeName)
+            });
+        }
+
+        function openDrillDown(officeId, categoryName, ruleType, officeName, columnId) {
+            var categoryId = l2CategoryIdMap[categoryName] || '';
+            var ruleLabel = columnId === 'deduction' ? '扣除' : (ruleType==1?'创建':ruleType==2?'审核':ruleType==10?'阅读':'点赞');
+            var title = (officeName || '公司级') + ' - ' + categoryName + '(' + ruleLabel + ') 文件明细';
+            var url = ctx + '/workKnowledgeBase/point/pointDrillDown?year=' + currentYear
+                + '&month=' + currentMonth
+                + '&officeId=' + encodeURIComponent(officeId)
+                + '&officeName=' + encodeURIComponent(officeName || '')
+                + '&categoryId=' + encodeURIComponent(categoryId)
+                + '&categoryName=' + encodeURIComponent(categoryName)
+                + '&ruleType=' + ruleType;
+            if (columnId) {
+                url += '&columnId=' + encodeURIComponent(columnId);
+            }
+            top.layer.open({
+                type: 2,
+                area: ['90%', '85%'],
+                title: title,
+                maxmin: true,
+                content: url
+            });
+        }
+
+        function showEmployeeDetail(userId, userName) {
+            $.ajax({
+                url: ctx + '/workKnowledgeBase/point/employeeSnapshot',
+                data: { year: currentYear, month: currentMonth, userId: userId },
+                type: 'GET',
+                dataType: 'json',
+                success: function(res) {
+                    if (res.success && res.data) {
+                        var d = res.data;
+                        var html = '<div style="padding:20px;">';
+                        html += '<h3 style="margin-bottom:15px;">' + escapeHtml(d.userName || userName) + ' - 积分明细(' + currentYear + '年' + currentMonth + '月)</h3>';
+                        html += '<table class="modal-table">';
+                        html += '<tr><th style="width:200px;">项目</th><th>积分值</th></tr>';
+                        html += '<tr><td>上传文档数</td><td>' + (d.uploadCount || 0) + '</td></tr>';
+                        html += '<tr><td>基础-阅读积分</td><td>' + (d.field1 || '0') + '</td></tr>';
+                        html += '<tr><td>基础-点赞积分</td><td>' + (d.field2 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-技术总结-创建</td><td>' + (d.field3 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-技术总结-审核</td><td>' + (d.field4 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-培训心得-创建</td><td>' + (d.field5 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-培训心得-审核</td><td>' + (d.field6 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-相关资料-创建</td><td>' + (d.field7 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-相关资料-审核</td><td>' + (d.field8 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-问答答疑-创建</td><td>' + (d.field9 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-问答答疑-回答</td><td>' + (d.field10 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-典型案例(原创)-创建</td><td>' + (d.field11 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-典型案例(原创)-审核</td><td>' + (d.field12 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-典型案例(转载)-创建</td><td>' + (d.field13 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-典型案例(转载)-审核</td><td>' + (d.field14 || '0') + '</td></tr>';
+                        html += '<tr><td>贡献-扣除积分</td><td class="' + (parseInt(d.field15||0) < 0 ? 'data-negative' : '') + '">' + (d.field15 || '0') + '</td></tr>';
+                        html += '</table></div>';
+                        top.layer.open({
+                            type: 1,
+                            area: ['500px', '600px'],
+                            title: '积分详情',
+                            content: html,
+                            shadeClose: true,
+                            maxmin: true
+                        });
+                    } else {
+                        top.layer.msg(res.message || '暂无详情数据', {icon: 0});
+                    }
+                }
+            });
+        }
+
+        function escapeHtml(str) {
+            if (!str) return '';
+            return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
+        }
+    </script>
+</head>
+<body>
+<div class="wrapper wrapper-content">
+    <!-- 年月选择栏 -->
+    <div class="year-month-bar">
+        <label>统计时间:</label>
+        <select id="selYear"></select>
+        <select id="selMonth"></select>
+        <button id="btnQuery" class="layui-btn layui-btn-sm layui-bg-blue">查询</button>
+        <button class="layui-btn layui-btn-sm layui-bg-orange" onclick="exportPointStats()">导出积分统计</button>
+        <span style="margin-left:15px;color:#999;font-size:12px;" id="dateRangeLabel"></span>
+    </div>
+
+    <!-- 公司级汇总 -->
+    <div class="dashboard-section">
+        <div class="section-title" id="companyTitle">公司级积分统计汇总</div>
+        <div id="companySummary" style="padding:10px;"></div>
+    </div>
+
+    <!-- 部门级汇总 -->
+    <div class="dashboard-section">
+        <div class="section-title" id="deptTitle">部门级积分统计汇总(点击部门名称查看员工明细)</div>
+        <div id="deptTable" style="padding:10px;"></div>
+    </div>
+</div>
+</body>
+</html>

+ 291 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointDeptEmployees.jsp

@@ -0,0 +1,291 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ include file="/webpage/include/taglib.jsp"%>
+<html>
+<head>
+    <title>${officeName} - 员工积分明细</title>
+    <meta name="decorator" content="default"/>
+    <script type="text/javascript" src="${ctxStatic}/layer-v2.3/layui/layui.all.js" charset="utf-8"></script>
+    <style>
+        .data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
+        .data-table th, .data-table td {
+            padding: 6px 10px; border: 1px solid #d2d2d2; text-align: center;
+        }
+        .data-table thead th { background: #f8f8f8; font-weight: bold; }
+        .data-table thead th.l1-header-basic { background: #d9edf7; }
+        .data-table thead th.l1-header-contrib { background: #fcf8e3; }
+        .data-table thead th.l2-header { background: #f0f0f0; }
+        .data-table tbody tr:hover { background: #f5f5f5; }
+        .data-negative { color: #FF5722; }
+        .data-positive { color: #1aa094; }
+        .clickable-num { color: #1E9FFF; cursor: pointer; text-decoration: underline; }
+        .clickable-num:hover { color: #009688; }
+    </style>
+    <script type="text/javascript">
+        var ctx = '${ctx}';
+        var currentYear = '${year}';
+        var currentMonth = '${month}';
+        var l2CategoryIdMap = {};
+        <c:forEach items="${l2CategoryIdMap}" var="entry">
+        l2CategoryIdMap['${entry.key}'] = '${entry.value}';
+        </c:forEach>
+
+        $(document).ready(function() {
+            // 初始化日期选择器
+            layui.use('laydate', function() {
+                var laydate = layui.laydate;
+                laydate.render({
+                    elem: '#beginDate',
+                    event: 'focus',
+                    type: 'date',
+                    format: 'yyyy-MM-dd',
+                    trigger: 'click',
+                    done: function(value) {
+                        if (value) {
+                            var parts = value.split('-');
+                            $('#yearVal').val(parts[0]);
+                            $('#monthVal').val(parseInt(parts[1]));
+                        }
+                    }
+                });
+                laydate.render({
+                    elem: '#endDate',
+                    event: 'focus',
+                    type: 'date',
+                    format: 'yyyy-MM-dd',
+                    trigger: 'click',
+                    done: function(value) {
+                        if (value) {
+                            var parts = value.split('-');
+                            $('#yearVal').val(parts[0]);
+                            $('#monthVal').val(parseInt(parts[1]));
+                        }
+                    }
+                });
+            });
+        });
+
+        function padZero(n) { return n < 10 ? '0' + n : '' + n; }
+
+        function search() {
+            $('#pageNo').val(1);
+            $('#searchForm').submit();
+        }
+
+        function resetSearch() {
+            // 清空树形选择器
+            $('#submitterId').val('');
+            $('#submitterName').val('');
+            $('#submitter').val('');
+            $('#searchOfficeId').val('');
+            $('#searchOfficeName').val('');
+            $('#beginDate').val('${beginDate}');
+            $('#endDate').val('${endDate}');
+            $('#yearVal').val('${year}');
+            $('#monthVal').val('${month}');
+            $('#searchForm').submit();
+        }
+
+        function openDrillDown(userId, categoryName, ruleType, columnId) {
+            var categoryId = l2CategoryIdMap[categoryName] || '';
+            var ruleLabel = columnId === 'deduction' ? '扣除' : (ruleType==1?'创建':ruleType==2?'审核':ruleType==10?'阅读':'点赞');
+            var title = categoryName + '(' + ruleLabel + ') 文件明细';
+            var url = ctx + '/workKnowledgeBase/point/pointDrillDown?year=' + currentYear
+                + '&month=' + currentMonth
+                + '&userId=' + encodeURIComponent(userId)
+                + '&categoryId=' + encodeURIComponent(categoryId)
+                + '&categoryName=' + encodeURIComponent(categoryName)
+                + '&ruleType=' + ruleType;
+            if (columnId) {
+                url += '&columnId=' + encodeURIComponent(columnId);
+            }
+            top.layer.open({
+                type: 2,
+                area: ['90%', '85%'],
+                title: title,
+                maxmin: true,
+                content: url
+            });
+        }
+
+        function showEmployeeDetail(userId, userName) {
+            $.ajax({
+                url: ctx + '/workKnowledgeBase/point/employeeSnapshot',
+                data: { year: currentYear, month: currentMonth, userId: userId },
+                type: 'GET',
+                dataType: 'json',
+                success: function(res) {
+                    if (res.success && res.data) {
+                        var d = res.data;
+                        var html = '<div style="padding:20px;">';
+                        html += '<h3 style="margin-bottom:15px;">' + userName + ' - 积分明细(' + currentYear + '年' + currentMonth + '月)</h3>';
+                        html += '<table style="width:100%;border-collapse:collapse;font-size:13px;">';
+                        html += '<tr><th style="width:200px;padding:8px 12px;border:1px solid #e6e6e6;background:#f8f8f8;">项目</th><th style="padding:8px 12px;border:1px solid #e6e6e6;">积分值</th></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">上传文档数</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.uploadCount || 0) + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">基础-阅读积分</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field1 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">基础-点赞积分</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field2 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-技术总结-创建</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field3 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-技术总结-审核</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field4 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-培训心得-创建</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field5 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-培训心得-审核</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field6 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-相关资料-创建</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field7 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-相关资料-审核</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field8 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-问答答疑-创建</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field9 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-问答答疑-回答</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field10 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-典型案例(原创)-创建</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field11 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-典型案例(原创)-审核</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field12 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-典型案例(转载)-创建</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field13 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-典型案例(转载)-审核</td><td style="padding:8px 12px;border:1px solid #e6e6e6;">' + (d.field14 || '0') + '</td></tr>';
+                        html += '<tr><td style="padding:8px 12px;border:1px solid #e6e6e6;">贡献-扣除积分</td><td style="padding:8px 12px;border:1px solid #e6e6e6;color:' + (parseInt(d.field15||0) < 0 ? '#FF5722' : 'inherit') + ';">' + (d.field15 || '0') + '</td></tr>';
+                        html += '</table></div>';
+                        top.layer.open({
+                            type: 1,
+                            area: ['500px', '600px'],
+                            title: '积分详情',
+                            content: html,
+                            shadeClose: true,
+                            maxmin: true
+                        });
+                    } else {
+                        top.layer.msg(res.message || '暂无详情数据', {icon: 0});
+                    }
+                }
+            });
+        }
+    </script>
+</head>
+<body>
+<div class="wrapper wrapper-content">
+    <div class="layui-row">
+        <!-- 查询条件区域 -->
+        <div class="full-width fl">
+            <div class="contentShadow layui-row" id="queryDiv">
+                <form id="searchForm" action="${ctx}/workKnowledgeBase/point/deptEmployeesPage" method="post" class="form-inline">
+                    <input id="pageNo" name="pageNo" type="hidden" value="1"/>
+                    <input id="pageSize" name="pageSize" type="hidden" value="30"/>
+                    <input type="hidden" name="officeId" value="${officeId}"/>
+                    <input id="yearVal" name="year" type="hidden" value="${year}"/>
+                    <input id="monthVal" name="month" type="hidden" value="${month}"/>
+                    <div class="commonQuery lw7">
+                        <div class="layui-item query athird">
+                            <label class="layui-form-label">人员:</label>
+                            <div class="layui-input-block with-icon">
+                                <sys:inquireselectUserNotReadolnyTow id="submitter" name="submitterId" value="${submitterId}" labelName="submitterName" labelValue="${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">
+                            <label class="layui-form-label">部门:</label>
+                            <div class="layui-input-block with-icon">
+                                <sys:treeselect id="searchOfficeId" name="searchOfficeId" value="${searchOfficeId}" labelName="searchOfficeName" labelValue="${searchOfficeName}" 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">
+                            <label class="layui-form-label">时间区间:</label>
+                            <div class="layui-input-block readOnlyFFF with-icon">
+                                <input id="beginDate" placeholder="开始时间" type="text" readonly="readonly"
+                                       maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+                                       value="${beginDate}"/>
+                                <span class="group-sep">-</span>
+                                <input id="endDate" placeholder="结束时间" type="text" readonly="readonly"
+                                       maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+                                       value="${endDate}"/>
+                            </div>
+                        </div>
+                        <div style="clear:both;"></div>
+                        <div class="layui-item athird" style="float:right;">
+                            <div class="layui-btn-group search-spacing">
+                                <button class="layui-btn layui-btn-sm layui-bg-blue" onclick="search()">查询</button>
+                                <button class="layui-btn layui-btn-sm" onclick="resetSearch()">重置</button>
+                            </div>
+                        </div>
+                        <div style="clear:both;"></div>
+                    </div>
+                </form>
+            </div>
+        </div>
+
+        <!-- 表格区域 -->
+        <div class="full-width fl">
+            <div class="contentShadow layui-form contentDetails">
+                <table class="data-table">
+                    <thead>
+                        <tr>
+                            <th rowspan="3">序号</th>
+                            <th rowspan="3">姓名</th>
+                            <th rowspan="3">上传文档数</th>
+                            <th colspan="5" class="l1-header-basic">基础积分</th>
+                            <th colspan="16" class="l1-header-contrib">贡献积分</th>
+                        </tr>
+                        <tr>
+                            <th rowspan="2" class="l2-header">阅读</th>
+                            <th rowspan="2" class="l2-header">点赞</th>
+                            <th rowspan="2" class="l2-header">积分汇总</th>
+                            <th rowspan="2" class="l2-header">已兑换积分</th>
+                            <th rowspan="2" class="l2-header">剩余积分</th>
+                            <th colspan="2" class="l2-header">技术总结</th>
+                            <th colspan="2" class="l2-header">培训心得</th>
+                            <th colspan="2" class="l2-header">相关资料</th>
+                            <th colspan="2" class="l2-header">问答答疑</th>
+                            <th colspan="2" class="l2-header">典型案例(原创)</th>
+                            <th colspan="2" class="l2-header">典型案例(转载)</th>
+                            <th rowspan="2" class="l2-header">扣除积分</th>
+                            <th rowspan="2" class="l2-header">积分汇总</th>
+                            <th rowspan="2" class="l2-header">已兑换积分</th>
+                            <th rowspan="2" class="l2-header">剩余积分</th>
+                        </tr>
+                        <tr>
+                            <th class="l2-header">创建</th><th class="l2-header">审核</th>
+                            <th class="l2-header">创建</th><th class="l2-header">审核</th>
+                            <th class="l2-header">创建</th><th class="l2-header">审核</th>
+                            <th class="l2-header">创建</th><th class="l2-header">回答</th>
+                            <th class="l2-header">创建</th><th class="l2-header">审核</th>
+                            <th class="l2-header">创建</th><th class="l2-header">审核</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <c:choose>
+                            <c:when test="${not empty empList}">
+                                <c:forEach items="${empList}" var="emp" varStatus="st">
+                                    <c:set var="s" value="${summaryMap[emp.userId]}"/>
+                                    <tr>
+                                        <td>${st.index + 1}</td>
+                                        <td>${emp.userName}</td>
+                                        <td>${emp.uploadCount != null ? emp.uploadCount : 0}</td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','阅读',10)">${emp.field1 != null ? emp.field1 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','点赞',20)">${emp.field2 != null ? emp.field2 : '0'}</span></td>
+                                        <td class="data-positive">${s != null ? s.basicSubtotal : 0}</td>
+                                        <td>${s != null ? s.basicExchange : 0}</td>
+                                        <td class="data-positive">${s != null ? s.basicRemaining : 0}</td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','技术总结',1)">${emp.field3 != null ? emp.field3 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','技术总结',2)">${emp.field4 != null ? emp.field4 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','培训心得',1)">${emp.field5 != null ? emp.field5 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','培训心得',2)">${emp.field6 != null ? emp.field6 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','相关资料',1)">${emp.field7 != null ? emp.field7 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','相关资料',2)">${emp.field8 != null ? emp.field8 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','问答答疑',1)">${emp.field9 != null ? emp.field9 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','问答答疑',2)">${emp.field10 != null ? emp.field10 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','典型案例(原创)',1)">${emp.field11 != null ? emp.field11 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','典型案例(原创)',2)">${emp.field12 != null ? emp.field12 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','典型案例(转载)',1)">${emp.field13 != null ? emp.field13 : '0'}</span></td>
+                                        <td><span class="clickable-num" onclick="openDrillDown('${emp.userId}','典型案例(转载)',2)">${emp.field14 != null ? emp.field14 : '0'}</span></td>
+                                        <td class="${emp.field15 != null && emp.field15 lt 0 ? 'data-negative' : ''}"><span class="clickable-num" onclick="openDrillDown('${emp.userId}','扣除积分',0,'deduction')">${emp.field15 != null ? emp.field15 : '0'}</span></td>
+                                        <td class="data-positive">${s != null ? s.contribSubtotal : 0}</td>
+                                        <td>${s != null ? s.contribExchange : 0}</td>
+                                        <td class="${s != null && s.contribRemaining != null && s.contribRemaining lt 0 ? 'data-negative' : ''}">${s != null ? s.contribRemaining : 0}</td>
+                                    </tr>
+                                </c:forEach>
+                            </c:when>
+                            <c:otherwise>
+                                <tr><td colspan="25" style="color:#999;">暂无数据</td></tr>
+                            </c:otherwise>
+                        </c:choose>
+                    </tbody>
+                </table>
+                <div style="clear:both;"></div>
+            </div>
+        </div>
+    </div>
+</div>
+</body>
+</html>

+ 279 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointDrillDown.jsp

@@ -0,0 +1,279 @@
+<%@ 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" src="${ctxStatic}/layer-v2.3/layui/layui.all.js" charset="utf-8"></script>
+    <style>
+        .data-positive { color: #1aa094; font-weight: bold; }
+        .data-negative { color: #FF5722; font-weight: bold; }
+    </style>
+    <script type="text/javascript">
+        var ctx = '${ctx}';
+
+        $(document).ready(function() {
+
+
+            // moresee 展开/收起动态字段查询区
+            $('#moresee').click(function(){
+                if($('#moresees').is(':visible')) {
+                    $('#moresees').slideUp(0, resizeListWindow1);
+                    $('#moresee i').removeClass('glyphicon glyphicon-menu-up').addClass('glyphicon glyphicon-menu-down');
+                } else {
+                    $('#moresees').slideDown(0, resizeListWindow1);
+                    $('#moresee i').removeClass('glyphicon glyphicon-menu-down').addClass('glyphicon glyphicon-menu-up');
+                }
+                return false;
+            });
+
+            // 初始化日期选择器
+            layui.use('laydate', function() {
+                var laydate = layui.laydate;
+                laydate.render({
+                    elem: '#beginDate',
+                    event: 'focus',
+                    type: 'date',
+                    format: 'yyyy-MM-dd',
+                    trigger: 'click'
+                });
+                laydate.render({
+                    elem: '#endDate',
+                    event: 'focus',
+                    type: 'date',
+                    format: 'yyyy-MM-dd',
+                    trigger: 'click'
+                });
+            });
+        });
+
+        /** 跳转到文档详情 */
+        function openShareDetail(shareId, treeNodeId) {
+            top.layer.open({
+                type: 2,
+                area: ['80%', '80%'],
+                title: '文件详情',
+                maxmin: true,
+                content: '${ctx}/workKnowledgeBase/share/detail?id=' + shareId + (treeNodeId ? '&treeNodeId=' + treeNodeId : ''),
+                btn: ['关闭'],
+                btn1: function(index) { top.layer.close(index); }
+            });
+        }
+
+        function search() {
+            $('#shareNameQuery').val($('#shareNameInput').val());
+            $('#pageNo').val(1);
+            $('#searchForm').submit();
+            return false;
+        }
+
+        function resetSearch() {
+            $('#shareNameInput').val('');
+            $('#beginDate').val('${beginDate}');
+            $('#endDate').val('${endDate}');
+            $('#shareNameQuery').val('');
+            // 清空树形选择器
+            $('#submitterId').val('');
+            $('#submitterName').val('');
+            $('#submitter').val('');
+            $('#searchOfficeId').val('');
+            $('#searchOfficeName').val('');
+            $('#pageNo').val(1);
+            $('#searchForm').submit();
+            return false;
+        }
+    </script>
+</head>
+<body>
+<div class="wrapper wrapper-content">
+    <div class="layui-row">
+        <!-- 查询条件区域 -->
+        <div class="full-width fl">
+            <div class="contentShadow layui-row" id="queryDiv">
+                <form id="searchForm" action="${ctx}/workKnowledgeBase/point/pointDrillDown" 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 type="hidden" name="year" value="${year}"/>
+                    <input type="hidden" name="month" value="${month}"/>
+                    <input type="hidden" name="officeId" value="${officeId}"/>
+                    <input type="hidden" name="officeName" value="${officeName}"/>
+                    <input type="hidden" name="userId" value="${userId}"/>
+                    <input type="hidden" name="categoryId" value="${categoryId}"/>
+                    <input type="hidden" name="categoryName" value="${categoryName}"/>
+                    <input type="hidden" name="ruleType" value="${ruleType}"/>
+                    <input type="hidden" name="columnId" value="${columnId}"/>
+                    <input id="shareNameQuery" name="shareName" type="hidden" value="${shareName}"/>
+                    <div class="commonQuery lw7">
+
+                        <div class="layui-item query athird">
+                            <label class="layui-form-label">文档名称:</label>
+                            <div class="layui-input-block with-icon">
+                                <input id="shareNameInput" type="text" value="${shareName}"
+                                       placeholder="文档名称模糊查询" class="form-control layui-input"/>
+                            </div>
+                        </div>
+
+                        <div class="layui-item query athird">
+                            <label class="layui-form-label">人员:</label>
+                            <div class="layui-input-block with-icon">
+                                <sys:inquireselectUserNotReadolnyTow id="submitter" name="submitterId" value="${submitterId}" labelName="submitterName" labelValue="${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 athird">
+                            <div class="input-group">
+                                <a href="#" id="moresee"><i class="glyphicon glyphicon-menu-down"></i></a>
+                                <div class="layui-btn-group search-spacing">
+                                    <button id="searchQuery" class="layui-btn layui-btn-sm layui-bg-blue" onclick="search()">查询</button>
+                                    <button id="searchReset" class="layui-btn layui-btn-sm" onclick="resetSearch()">重置</button>
+                                </div>
+                            </div>
+                        </div>
+                        <div style="clear:both;"></div>
+                    </div>
+                    <div id="moresees" style="clear:both;display:none;" class="lw7">
+
+                        <div class="layui-item query athird">
+                            <label class="layui-form-label">部门:</label>
+                            <div class="layui-input-block with-icon">
+                                <sys:treeselect id="searchOfficeId" name="searchOfficeId" value="${searchOfficeId}" labelName="searchOfficeName" labelValue="${searchOfficeName}" 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">
+                            <label class="layui-form-label">时间区间:</label>
+                            <div class="layui-input-block readOnlyFFF with-icon">
+                                <input id="beginDate" placeholder="开始时间" type="text" readonly="readonly"
+                                       maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+                                       value="${beginDate}"/>
+                                <span class="group-sep">-</span>
+                                <input id="endDate" placeholder="结束时间" type="text" readonly="readonly"
+                                       maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
+                                       value="${endDate}"/>
+                            </div>
+                        </div>
+
+
+                        <div style="clear:both;"></div>
+                    </div>
+                </form>
+            </div>
+        </div>
+
+        <!-- 数据表格区域 -->
+        <div class="full-width fl">
+            <div class="contentShadow layui-form contentDetails">
+                <div class="nav-btns">
+                    <div class="layui-btn-group">
+                        <span style="margin-left:5px;color:#666;font-size:13px;line-height:30px;">
+                            共 <span style="color:#1E9FFF;font-weight:bold;">${page.count}</span> 条记录
+                            <c:choose>
+                                <c:when test="${columnId == 'deduction'}">
+                                    &nbsp;|&nbsp;<span style="color:#FF5722;">扣除积分</span>
+                                </c:when>
+                                <c:otherwise>
+                                    &nbsp;|&nbsp;${categoryName}
+                                    (<c:choose>
+                                        <c:when test="${ruleType == 1}">创建</c:when>
+                                        <c:when test="${ruleType == 2}">审核</c:when>
+                                        <c:when test="${ruleType == 10}">阅读</c:when>
+                                        <c:when test="${ruleType == 20}">点赞</c:when>
+                                        <c:otherwise>全部</c:otherwise>
+                                    </c:choose>)
+                                </c:otherwise>
+                            </c:choose>
+                        </span>
+                    </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>
+    layui.use('table', function(){
+        layui.table.render({
+            limit:${ page.pageSize }
+            ,id:"drillDownTable"
+            ,elem: '#contentTable'
+            ,page: false
+            ,cols: [[
+                {field:'index',align:'center', width:60,title: '序号'}
+                ,{field:'userName',align:'center', title: '姓名', width:100}
+                ,{field:'officeName',align:'center', title: '部门', width:140}
+                ,{field:'shareName',align:'center', title: '文档名称', minWidth:240,templet:function(d){
+                    var nm = d.shareName || '';
+                    var sid = d.shareId || '';
+                    if (!sid) {
+                        return '<span style="color:#999;">(无关联文档)</span>';
+                    }
+                    if (d.shareDelFlag === '1' || !nm) {
+                        return '<span style="color:#999;">' + (nm || '(已删除文档)') + '</span>';
+                    }
+                    return '<a class="attention-info" href="javascript:void(0)" onclick="openShareDetail(\'' + sid + '\',\'' + (d.treeNodeId || '') + '\')">' + nm + '</a>';
+                }}
+                ,{field:'categoryName',align:'center', title: '来源分类', width:140,templet:function(d){
+                    return d.categoryName || '-';
+                }}
+                ,{field:'ruleType',align:'center', title: '获取方式', width:110,templet:function(d){
+                    var rt = parseInt(d.ruleType);
+                    if (rt === 1) return '<span class="layui-badge layui-bg-blue">创建</span>';
+                    if (rt === 2) return '<span class="layui-badge layui-bg-green">审核</span>';
+                    if (rt === 10) return '<span class="layui-badge layui-bg-orange">阅读</span>';
+                    if (rt === 20) return '<span class="layui-badge layui-bg-red">点赞</span>';
+                    return '-';
+                }}
+                ,{field:'point',align:'center', title: '积分额度', width:100,templet:function(d){
+                    var val = d.point || 0;
+                    var prefix = val >= 0 ? '+' : '';
+                    var color = val >= 0 ? '#1aa094' : '#ff5722';
+                    return '<span style="color:' + color + ';font-weight:bold;">' + prefix + val + '</span>';
+                }}
+                ,{field:'pointCreateDate',align:'center', title: '时间', width:170}
+            ]]
+            ,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}"
+                                ,"id": "${row.id}"
+                                ,"userName": "<c:out value='${row.userName}'/>"
+                                ,"officeName": "<c:out value='${row.officeName}'/>"
+                                ,"shareId": "${row.shareId}"
+                                ,"shareName": "<c:out value='${row.shareName}'/>"
+                                ,"treeNodeId": "${row.treeNodeId}"
+                                ,"shareDelFlag": "${row.shareDelFlag}"
+                                ,"categoryId": "${row.categoryId}"
+                                ,"ruleId": "${row.ruleId}"
+                                ,"categoryName": "<c:out value='${row.categoryName}'/>"
+                                ,"ruleType": "${row.ruleType}"
+                                ,"point": "${row.point}"
+                                ,"pointCreateDate": "<fmt:formatDate value="${row.pointCreateDate}" pattern="yyyy-MM-dd"/>"
+                            }
+                        </c:forEach>
+                    </c:when>
+                    <c:otherwise></c:otherwise>
+                </c:choose>
+            ]
+        });
+    });
+
+    // 回车触发查询
+    $('#shareNameInput').keydown(function(e) { if (e.keyCode === 13) { search(); } });
+
+    resizeListWindow1();
+</script>
+<script>
+    resizeListWindow1();
+    $(window).resize(function () {
+        resizeListWindow1();
+    });
+</script>
+</body>
+</html>

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

@@ -123,14 +123,14 @@
 					<form:hidden path="toflag" value="${workKnowledgeBasePoint.toflag}"/>
 					<div class="commonQuery lw7">
 						<div class="layui-item query athird" style="width: 25%">
-							<label class="layui-form-label">经办人:</label>
+							<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>
+							<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" />

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

@@ -849,15 +849,15 @@
                         <div class="layui-item query athird">
                             <label class="layui-form-label">所属人:</label>
                             <div class="layui-input-block with-icon">
-                                <input type="text" name="ownerUserName" value="${shareInfo.ownerUserName}"
-                                       placeholder="所属人姓名模糊查询" class="form-control layui-input"/>
+                                <sys:inquireselectUserNotReadolnyTow id="ownerUser" name="ownerUserId" value="${shareInfo.ownerUserId}" labelName="ownerUserName" labelValue="${shareInfo.ownerUserName}" 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">
                             <label class="layui-form-label">所属人部门:</label>
                             <div class="layui-input-block with-icon">
-                                <input type="text" name="ownerOfficeName" value="${shareInfo.ownerOfficeName}"
-                                       placeholder="所属人部门名称模糊查询" class="form-control layui-input"/>
+                                <sys:treeselect id="ownerOfficeId" name="ownerOfficeId" value="${shareInfo.ownerOfficeId}" labelName="ownerOfficeName" labelValue="${shareInfo.ownerOfficeName}" 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">