Browse Source

知识库,积分兑换功能

徐滕 3 weeks ago
parent
commit
49cb503d98
16 changed files with 1029 additions and 23 deletions
  1. 26 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBasePointDetailDao.java
  2. 32 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseUserExchangeFlagDao.java
  3. 11 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/UserPointDetail.java
  4. 88 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBasePoint.java
  5. 47 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseUserExchangeFlag.java
  6. 11 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBasePointDetailService.java
  7. 397 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBasePointExchangeService.java
  8. 30 1
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseReadLikeService.java
  9. 29 1
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java
  10. 81 2
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBasePointController.java
  11. 0 2
      src/main/java/com/jeeplus/modules/workreimbursement/web/WorkReimbursementController.java
  12. 84 8
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml
  13. 53 0
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseUserExchangeFlagDao.xml
  14. 53 0
      src/main/resources/mysqlDateBase/add_exchange_fields.sql
  15. 67 2
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointList.jsp
  16. 20 7
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointUserDetail.jsp

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

@@ -35,6 +35,13 @@ public interface WorkKnowledgeBasePointDetailDao extends CrudDao<WorkKnowledgeBa
         @Param("columnId") String columnId
     );
 
+    /** 根据用户ID、分类ID和积分类型统计积分总和 */
+    int sumUserPointByColumnIdWithPointType(
+        @Param("userId") String userId,
+        @Param("columnId") String columnId,
+        @Param("pointType") String pointType
+    );
+
     /**
      * 物理删除指定条件的积分记录(用于取消点赞时删除)
      * @param userId 用户ID(积分接收人)
@@ -46,4 +53,23 @@ public interface WorkKnowledgeBasePointDetailDao extends CrudDao<WorkKnowledgeBa
         @Param("shareId") String shareId,
         @Param("changeType") Integer changeType
     );
+
+    /**
+     * 获取所有有积分的用户ID列表(去重)
+     * @return 用户ID列表
+     */
+    List<String> findAllUserIdsWithPoints();
+
+    /**
+     * 根据分类ID获取有该分类积分的用户ID列表(去重)
+     * @param columnId 分类ID(1-基础积分,2-贡献积分)
+     * @return 用户ID列表
+     */
+    List<String> findUserIdsByColumnId(@Param("columnId") String columnId);
+
+    /**
+     * 查询所有用户的基础积分和贡献积分(用于导出)
+     * @return 用户积分列表,每行包含 userId/userName/officeName/mobile/basicPoint/contributionPoint
+     */
+    List<WorkKnowledgeBasePoint> findAllUserPointSummary();
 }

+ 32 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseUserExchangeFlagDao.java

@@ -0,0 +1,32 @@
+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.WorkKnowledgeBaseUserExchangeFlag;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * 知识库用户积分兑换标记 DAO
+ * @author: 徐滕
+ * @version: 2026-06-18
+ */
+@MyBatisDao
+public interface WorkKnowledgeBaseUserExchangeFlagDao extends CrudDao<WorkKnowledgeBaseUserExchangeFlag> {
+
+    /**
+     * 根据用户ID获取首次兑换标记
+     * @param userId 用户ID
+     * @return 兑换标记对象,如果不存在返回null
+     */
+    WorkKnowledgeBaseUserExchangeFlag getByUserId(@Param("userId") String userId);
+
+    /**
+     * 更新用户的首次兑换状态
+     * @param userId 用户ID
+     * @param isFirstExchanged 是否已兑换(0=未兑换,1=已兑换)
+     */
+    void updateFirstExchangeStatus(
+        @Param("userId") String userId,
+        @Param("isFirstExchanged") Integer isFirstExchanged
+    );
+}

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

@@ -54,6 +54,9 @@ public class UserPointDetail extends DataEntity<UserPointDetail> {
     private String userName;
     private String columnId;
 
+    /** 积分类型筛选:acquire-积分获取,exchange-积分兑换 */
+    private String pointType;
+
     public String getId() {
         return id;
     }
@@ -181,4 +184,12 @@ public class UserPointDetail extends DataEntity<UserPointDetail> {
     public void setColumnId(String columnId) {
         this.columnId = columnId;
     }
+
+    public String getPointType() {
+        return pointType;
+    }
+
+    public void setPointType(String pointType) {
+        this.pointType = pointType;
+    }
 }

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

@@ -46,6 +46,30 @@ public class WorkKnowledgeBasePoint extends DataEntity<WorkKnowledgeBasePoint> {
 
 	private Date gainBeginDate;
 	private Date gainEndDate;
+
+	/** 基础积分(当前可用) */
+	private Integer basicPoint;
+
+	/** 贡献积分(当前可用) */
+	private Integer contributionPoint;
+
+	/** 基础积分兑换量 */
+	private Integer exchangedBasicPoint;
+
+	/** 贡献积分兑换量 */
+	private Integer exchangedContributionPoint;
+
+	/** 基础积分剩余 */
+	private Integer remainingBasicPoint;
+
+	/** 贡献积分剩余 */
+	private Integer remainingContributionPoint;
+
+	/** 原始基础积分(兑换前,用于显示可兑换积分) */
+	private Integer originalBasicPoint;
+
+	/** 原始贡献积分(兑换前,用于显示可兑换积分) */
+	private Integer originalContributionPoint;
 	
 	public WorkKnowledgeBasePoint() {
 		super();
@@ -214,4 +238,68 @@ public class WorkKnowledgeBasePoint extends DataEntity<WorkKnowledgeBasePoint> {
 	public void setGainEndDate(Date gainEndDate) {
 		this.gainEndDate = gainEndDate;
 	}
+
+	public Integer getBasicPoint() {
+		return basicPoint;
+	}
+
+	public void setBasicPoint(Integer basicPoint) {
+		this.basicPoint = basicPoint;
+	}
+
+	public Integer getContributionPoint() {
+		return contributionPoint;
+	}
+
+	public void setContributionPoint(Integer contributionPoint) {
+		this.contributionPoint = contributionPoint;
+	}
+
+	public Integer getExchangedBasicPoint() {
+		return exchangedBasicPoint;
+	}
+
+	public void setExchangedBasicPoint(Integer exchangedBasicPoint) {
+		this.exchangedBasicPoint = exchangedBasicPoint;
+	}
+
+	public Integer getExchangedContributionPoint() {
+		return exchangedContributionPoint;
+	}
+
+	public void setExchangedContributionPoint(Integer exchangedContributionPoint) {
+		this.exchangedContributionPoint = exchangedContributionPoint;
+	}
+
+	public Integer getRemainingBasicPoint() {
+		return remainingBasicPoint;
+	}
+
+	public void setRemainingBasicPoint(Integer remainingBasicPoint) {
+		this.remainingBasicPoint = remainingBasicPoint;
+	}
+
+	public Integer getRemainingContributionPoint() {
+		return remainingContributionPoint;
+	}
+
+	public void setRemainingContributionPoint(Integer remainingContributionPoint) {
+		this.remainingContributionPoint = remainingContributionPoint;
+	}
+
+	public Integer getOriginalBasicPoint() {
+		return originalBasicPoint;
+	}
+
+	public void setOriginalBasicPoint(Integer originalBasicPoint) {
+		this.originalBasicPoint = originalBasicPoint;
+	}
+
+	public Integer getOriginalContributionPoint() {
+		return originalContributionPoint;
+	}
+
+	public void setOriginalContributionPoint(Integer originalContributionPoint) {
+		this.originalContributionPoint = originalContributionPoint;
+	}
 }

+ 47 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseUserExchangeFlag.java

@@ -0,0 +1,47 @@
+package com.jeeplus.modules.WorkKnowledgeBase.entity;
+
+import com.jeeplus.common.persistence.DataEntity;
+
+import java.util.Date;
+
+/**
+ * 知识库用户积分兑换标记表实体
+ * 对应表 work_knowledge_base_user_exchange_flag
+ * @author: 徐滕
+ * @version: 2026-06-18
+ */
+public class WorkKnowledgeBaseUserExchangeFlag extends DataEntity<WorkKnowledgeBaseUserExchangeFlag> {
+
+    /** 用户ID */
+    private String userId;
+
+    /** 基础积分是否首次兑换:0=未兑换,1=已兑换 */
+    private Integer isFirstExchanged;
+
+    /** 首次兑换时间 */
+    private Date firstExchangeDate;
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public Integer getIsFirstExchanged() {
+        return isFirstExchanged;
+    }
+
+    public void setIsFirstExchanged(Integer isFirstExchanged) {
+        this.isFirstExchanged = isFirstExchanged;
+    }
+
+    public Date getFirstExchangeDate() {
+        return firstExchangeDate;
+    }
+
+    public void setFirstExchangeDate(Date firstExchangeDate) {
+        this.firstExchangeDate = firstExchangeDate;
+    }
+}

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

@@ -100,4 +100,15 @@ public class WorkKnowledgeBasePointDetailService extends CrudService<WorkKnowled
     public int sumUserPointByColumnId(String userId, String columnId) {
         return pointDetailDao.sumUserPointByColumnId(userId, columnId);
     }
+
+    /**
+     * 根据用户ID、分类ID和积分类型统计积分总和
+     * @param userId 用户ID
+     * @param columnId 分类ID
+     * @param pointType 积分类型(acquire-获取,exchange-兑换,null-全部)
+     * @return 积分总和
+     */
+    public int sumUserPointByColumnId(String userId, String columnId, String pointType) {
+        return pointDetailDao.sumUserPointByColumnIdWithPointType(userId, columnId, pointType);
+    }
 }

+ 397 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBasePointExchangeService.java

@@ -0,0 +1,397 @@
+package com.jeeplus.modules.WorkKnowledgeBase.service;
+
+import com.jeeplus.common.service.CrudService;
+import com.jeeplus.common.utils.StringUtils;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgeBasePointDetailDao;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgeBasePointRuleDao;
+import com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgeBaseUserExchangeFlagDao;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.*;
+import com.jeeplus.modules.sys.entity.User;
+import com.jeeplus.modules.sys.utils.UserUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 知识库积分兑换 Service
+ * 提供:基础积分兑换、贡献积分兑换、兑换记录查询等功能
+ * @author: 徐滕
+ * @version: 2026-06-18
+ */
+@Service
+@Transactional(readOnly = true)
+public class WorkKnowledgeBasePointExchangeService extends CrudService<WorkKnowledgeBasePointDetailDao, WorkKnowledgeBasePointDetail> {
+
+    @Autowired
+    private WorkKnowledgeBasePointDetailDao pointDetailDao;
+
+    @Autowired
+    private WorkKnowledgeBaseUserExchangeFlagDao exchangeFlagDao;
+
+    @Autowired
+    private WorkKnowledgeBasePointRuleDao pointRuleDao;
+
+    /**
+     * 获取用户当前可用基础积分(分类ID=1)
+     * @param userId 用户ID
+     * @return 可用基础积分
+     */
+    public int getUserBasicPoints(String userId) {
+        return pointDetailDao.sumUserPointByColumnId(userId, "1");
+    }
+
+    /**
+     * 获取用户当前可用贡献积分(分类ID=2)
+     * @param userId 用户ID
+     * @return 可用贡献积分
+     */
+    public int getUserContributionPoints(String userId) {
+        return pointDetailDao.sumUserPointByColumnId(userId, "2");
+    }
+
+    /**
+     * 执行基础积分兑换
+     * @param userId 用户ID
+     * @return 兑换结果信息
+     */
+    @Transactional(readOnly = false)
+    public String executeBasicPointExchange(String userId) {
+        // 1. 获取用户当前可用基础积分
+        int basicPoints = getUserBasicPoints(userId);
+
+        // 2. 获取用户首次兑换标记
+        WorkKnowledgeBaseUserExchangeFlag flag = exchangeFlagDao.getByUserId(userId);
+        if (flag == null) {
+            // 如果不存在标记,创建默认标记(未兑换)
+            flag = new WorkKnowledgeBaseUserExchangeFlag();
+            flag.setId(com.jeeplus.common.utils.IdGen.uuid());
+            flag.setUserId(userId);
+            flag.setIsFirstExchanged(0);
+            flag.setDelFlag("0");
+            flag.preInsert();
+            exchangeFlagDao.insert(flag);
+        }
+
+        boolean isFirstExchange = (flag.getIsFirstExchanged() == 0 || flag.getIsFirstExchanged() == null);
+
+        // 3. 读取兑换规则配置
+        // rule_type=91: 首次兑换门槛(默认300)
+        // rule_type=92: 非首次兑换门槛(默认100)
+        // rule_type=93: 累积步长(默认100)
+        Integer firstThreshold = getRuleValueByType("91"); // 首次门槛
+        Integer nonFirstThreshold = getRuleValueByType("92"); // 非首次门槛
+        Integer stepValue = getRuleValueByType("93"); // 累积步长
+
+        if (firstThreshold == null) firstThreshold = 300;
+        if (nonFirstThreshold == null) nonFirstThreshold = 100;
+        if (stepValue == null) stepValue = 100;
+
+        // 4. 判断是否满足兑换条件并计算可兑换额度
+        int exchangeablePoints = 0;
+        String ruleTypeName = "";
+
+        if (isFirstExchange) {
+            // 首次兑换:需要 >= 300
+            if (basicPoints < firstThreshold) {
+                return "基础积分不足" + firstThreshold + "分,无法进行首次兑换(当前" + basicPoints + "分)";
+            }
+            // 可兑换额度 = 向下取整百
+            exchangeablePoints = (basicPoints / stepValue) * stepValue;
+            ruleTypeName = "首次兑换";
+        } else {
+            // 非首次兑换:需要 >= 100
+            if (basicPoints < nonFirstThreshold) {
+                return "基础积分不足" + nonFirstThreshold + "分,无法进行兑换(当前" + basicPoints + "分)";
+            }
+            // 可兑换额度 = 向下取整百
+            exchangeablePoints = (basicPoints / stepValue) * stepValue;
+            ruleTypeName = "非首次兑换";
+        }
+
+        if (exchangeablePoints <= 0) {
+            return "无可兑换积分";
+        }
+
+        // 5. 创建兑换扣减记录(change_type=9)
+        WorkKnowledgeBasePointDetail detail = new WorkKnowledgeBasePointDetail();
+        detail.setId(com.jeeplus.common.utils.IdGen.uuid());
+        detail.setUserId(userId);
+        detail.setChangeType(9); // 9表示兑换扣减
+        detail.setCategoryId("9"); // 兑换积分分类ID
+        detail.setPoint(-exchangeablePoints); // 负数表示扣减
+        detail.setDelFlag("0");
+        detail.preInsert();
+        pointDetailDao.insert(detail);
+
+        // 6. 如果是首次兑换,更新标记为已兑换
+        if (isFirstExchange) {
+            exchangeFlagDao.updateFirstExchangeStatus(userId, 1);
+        }
+
+        return "兑换成功!" + ruleTypeName + "扣除" + exchangeablePoints + "分,剩余" + (basicPoints - exchangeablePoints) + "分";
+    }
+
+    /**
+     * 执行贡献积分兑换(全额清零)
+     * @param userId 用户ID
+     * @return 兑换结果信息
+     */
+    @Transactional(readOnly = false)
+    public String executeContributionPointExchange(String userId) {
+        // 1. 获取用户当前可用贡献积分
+        int contributionPoints = getUserContributionPoints(userId);
+
+        if (contributionPoints <= 0) {
+            return "无可用贡献积分,无法兑换";
+        }
+
+        // 2. 创建兑换扣减记录(change_type=9)
+        WorkKnowledgeBasePointDetail detail = new WorkKnowledgeBasePointDetail();
+        detail.setId(com.jeeplus.common.utils.IdGen.uuid());
+        detail.setUserId(userId);
+        detail.setChangeType(9); // 9表示兑换扣减
+        detail.setCategoryId("9"); // 兑换积分分类ID
+        detail.setPoint(-contributionPoints); // 负数表示扣减,全额清零
+        detail.setDelFlag("0");
+        detail.preInsert();
+        pointDetailDao.insert(detail);
+
+        return "兑换成功!贡献积分全额扣除" + contributionPoints + "分,剩余0分";
+    }
+
+    /**
+     * 批量执行所有用户的基础积分兑换
+     * @return 兑换结果统计
+     */
+    @Transactional(readOnly = false)
+    public String batchExecuteBasicPointExchange() {
+        // 获取所有有积分的用户列表
+        List<String> userIdList = pointDetailDao.findAllUserIdsWithPoints();
+        
+        if (userIdList == null || userIdList.isEmpty()) {
+            return "没有可兑换的用户";
+        }
+
+        int successCount = 0;
+        int failCount = 0;
+        StringBuilder resultMsg = new StringBuilder();
+        resultMsg.append("批量兑换完成:\n");
+
+        for (String userId : userIdList) {
+            try {
+                String result = executeBasicPointExchange(userId);
+                if (result.contains("兑换成功")) {
+                    successCount++;
+                    resultMsg.append("用户[").append(userId).append("]: ").append(result).append("\n");
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                failCount++;
+                resultMsg.append("用户[").append(userId).append("]: 兑换失败 - ").append(e.getMessage()).append("\n");
+            }
+        }
+
+        resultMsg.append("\n总计:成功").append(successCount).append("人,失败").append(failCount).append("人");
+        return resultMsg.toString();
+    }
+
+    /**
+     * 批量执行所有用户的贡献积分兑换
+     * @return 兑换结果统计
+     */
+    @Transactional(readOnly = false)
+    public String batchExecuteContributionPointExchange() {
+        // 获取所有有积分的用户列表
+        List<String> userIdList = pointDetailDao.findAllUserIdsWithPoints();
+        
+        if (userIdList == null || userIdList.isEmpty()) {
+            return "没有可兑换的用户";
+        }
+
+        int successCount = 0;
+        int failCount = 0;
+        StringBuilder resultMsg = new StringBuilder();
+        resultMsg.append("批量兑换完成:\n");
+
+        for (String userId : userIdList) {
+            try {
+                String result = executeContributionPointExchange(userId);
+                if (result.contains("兑换成功")) {
+                    successCount++;
+                    resultMsg.append("用户[").append(userId).append("]: ").append(result).append("\n");
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                failCount++;
+                resultMsg.append("用户[").append(userId).append("]: 兑换失败 - ").append(e.getMessage()).append("\n");
+            }
+        }
+
+        resultMsg.append("\n总计:成功").append(successCount).append("人,失败").append(failCount).append("人");
+        return resultMsg.toString();
+    }
+
+    /**
+     * 批量兑换并获取导出数据(按指定类型)
+     * @param exchangeType 兑换类型:basic-基础积分, contribution-贡献积分
+     * @return 导出用的用户积分数据列表(只包含参与兑换的用户)
+     */
+    @Transactional(readOnly = false)
+    public List<WorkKnowledgeBasePoint> batchExchangeForExport(String exchangeType) {
+        // 1. 根据兑换类型获取有对应积分的用户ID列表
+        String columnId = "basic".equals(exchangeType) ? "1" : "2";
+        List<String> userIdList = pointDetailDao.findUserIdsByColumnId(columnId);
+
+        if (userIdList == null || userIdList.isEmpty()) {
+            return new java.util.ArrayList<>();
+        }
+
+        // 2. 读取兑换规则
+        Integer firstThreshold = getRuleValueByType("91");
+        Integer nonFirstThreshold = getRuleValueByType("92");
+        Integer stepValue = getRuleValueByType("93");
+        if (firstThreshold == null) firstThreshold = 300;
+        if (nonFirstThreshold == null) nonFirstThreshold = 100;
+        if (stepValue == null) stepValue = 100;
+
+        // 记录每个用户的兑换量
+        Map<String, Integer> exchangedBasicMap = new HashMap<>();
+        Map<String, Integer> exchangedContribMap = new HashMap<>();
+        // 记录每个用户的原始积分(兑换前的值,用于显示“可兑换积分”)
+        Map<String, Integer> originalBasicMap = new HashMap<>();
+        Map<String, Integer> originalContribMap = new HashMap<>();
+        // 记录参与兑换的用户ID(用于过滤导出列表)
+        java.util.Set<String> exchangedUserIds = new java.util.HashSet<>();
+
+        // 3. 逐用户执行兑换(根据类型决定兑换哪种)
+        for (String userId : userIdList) {
+            try {
+                // === 基础积分兑换 ===
+                if ("basic".equals(exchangeType)) {
+                    int basicPoints = pointDetailDao.sumUserPointByColumnId(userId, "1");
+                    // 记录原始基础积分(兑换前)
+                    originalBasicMap.put(userId, basicPoints);
+                    
+                    WorkKnowledgeBaseUserExchangeFlag flag = exchangeFlagDao.getByUserId(userId);
+                    if (flag == null) {
+                        flag = new WorkKnowledgeBaseUserExchangeFlag();
+                        flag.setId(com.jeeplus.common.utils.IdGen.uuid());
+                        flag.setUserId(userId);
+                        flag.setIsFirstExchanged(0);
+                        flag.setDelFlag("0");
+                        flag.preInsert();
+                        exchangeFlagDao.insert(flag);
+                    }
+                    boolean isFirstExchange = (flag.getIsFirstExchanged() == 0 || flag.getIsFirstExchanged() == null);
+                    int threshold = isFirstExchange ? firstThreshold : nonFirstThreshold;
+                    int basicExchanged = 0;
+                    if (basicPoints >= threshold) {
+                        basicExchanged = (basicPoints / stepValue) * stepValue;
+                        if (basicExchanged > 0) {
+                            WorkKnowledgeBasePointDetail detail = new WorkKnowledgeBasePointDetail();
+                            detail.setId(com.jeeplus.common.utils.IdGen.uuid());
+                            detail.setUserId(userId);
+                            detail.setChangeType(9);
+                            detail.setCategoryId("1"); // 基础积分分类,使扣减自然计入基础积分汇总
+                            detail.setPoint(-basicExchanged);
+                            detail.setDelFlag("0");
+                            detail.preInsert();
+                            pointDetailDao.insert(detail);
+                            if (isFirstExchange) {
+                                exchangeFlagDao.updateFirstExchangeStatus(userId, 1);
+                            }
+                            // 只有实际兑换了才加入集合
+                            exchangedUserIds.add(userId);
+                        }
+                    }
+                    exchangedBasicMap.put(userId, basicExchanged);
+                }
+
+                // === 贡献积分兑换(全额清零) ===
+                if ("contribution".equals(exchangeType)) {
+                    int contributionPoints = pointDetailDao.sumUserPointByColumnId(userId, "2");
+                    // 记录原始贡献积分(兑换前)
+                    originalContribMap.put(userId, contributionPoints);
+                    
+                    int contribExchanged = 0;
+                    if (contributionPoints > 0) {
+                        contribExchanged = contributionPoints;
+                        WorkKnowledgeBasePointDetail cpDetail = new WorkKnowledgeBasePointDetail();
+                        cpDetail.setId(com.jeeplus.common.utils.IdGen.uuid());
+                        cpDetail.setUserId(userId);
+                        cpDetail.setChangeType(9);
+                        cpDetail.setCategoryId("2"); // 贡献积分分类,使扣减自然计入贡献积分汇总
+                        cpDetail.setPoint(-contribExchanged);
+                        cpDetail.setDelFlag("0");
+                        cpDetail.preInsert();
+                        pointDetailDao.insert(cpDetail);
+                        // 贡献积分只要有就兑换,加入集合
+                        exchangedUserIds.add(userId);
+                    }
+                    exchangedContribMap.put(userId, contribExchanged);
+                }
+            } catch (Exception e) {
+                // 单个用户失败不影响其他用户,继续处理
+                continue;
+            }
+        }
+
+        // 4. 查询所有用户的积分数据(此时数据已反映兑换后的状态)
+        List<WorkKnowledgeBasePoint> allUsers = pointDetailDao.findAllUserPointSummary();
+
+        // 5. 过滤出参与兑换的用户,并设置兑换量和剩余量
+        List<WorkKnowledgeBasePoint> exportList = new java.util.ArrayList<>();
+        for (WorkKnowledgeBasePoint item : allUsers) {
+            // 只保留参与兑换的用户
+            if (!exchangedUserIds.contains(item.getUserId())) {
+                continue;
+            }
+            
+            int exBasic = exchangedBasicMap.containsKey(item.getUserId()) ? exchangedBasicMap.get(item.getUserId()) : 0;
+            int exContrib = exchangedContribMap.containsKey(item.getUserId()) ? exchangedContribMap.get(item.getUserId()) : 0;
+            // basicPoint/contributionPoint 已是兑换后的剩余值(因为兑换记录的category_id已正确归类)
+            int remainBasic = item.getBasicPoint() != null ? item.getBasicPoint() : 0;
+            int remainContrib = item.getContributionPoint() != null ? item.getContributionPoint() : 0;
+            // 兑换前的原始积分(可兑换积分)
+            int origBasic = originalBasicMap.containsKey(item.getUserId()) ? originalBasicMap.get(item.getUserId()) : 0;
+            int origContrib = originalContribMap.containsKey(item.getUserId()) ? originalContribMap.get(item.getUserId()) : 0;
+
+            item.setExchangedBasicPoint(exBasic);
+            item.setExchangedContributionPoint(exContrib);
+            item.setRemainingBasicPoint(remainBasic);
+            item.setRemainingContributionPoint(remainContrib);
+            // 设置原始积分(可兑换积分)到新字段,不覆盖basicPoint/contributionPoint
+            item.setOriginalBasicPoint(origBasic);
+            item.setOriginalContributionPoint(origContrib);
+            
+            exportList.add(item);
+        }
+
+        return exportList;
+    }
+
+    /**
+     * 根据rule_type获取规则配置的point_value值
+     * @param ruleType 规则类型(91/92/93)
+     * @return 规则值,如果不存在返回null
+     */
+    private Integer getRuleValueByType(String ruleType) {
+        // 从 work_knowledge_base_point_rule 表查询
+        WorkKnowledgeBasePointRule rule = pointRuleDao.findByRuleType(Integer.parseInt(ruleType));
+        if (rule != null && StringUtils.isNotBlank(rule.getPointValue())) {
+            try {
+                return Integer.parseInt(rule.getPointValue());
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+}

+ 30 - 1
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseReadLikeService.java

@@ -10,6 +10,7 @@ import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePointDetail
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePointRule;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseReadRecord;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseShareInfo;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseUserExchangeFlag;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgePointCategory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -44,6 +45,9 @@ public class WorkKnowledgeBaseReadLikeService extends CrudService<WorkKnowledgeB
     @Autowired
     private WorkKnowledgeBasePointDetailDao pointDetailDao;
 
+    @Autowired
+    private WorkKnowledgeBaseUserExchangeFlagDao exchangeFlagDao;
+
     /**
      * 记录阅读行为(首次阅读给创建人加分)
      * @param fileId 文件ID
@@ -319,7 +323,7 @@ public class WorkKnowledgeBaseReadLikeService extends CrudService<WorkKnowledgeB
     }
 
     /**
-     * 插入积分明细记录
+     * 插入积分明细记录,同时确保该用户的兑换标记记录存在
      */
     private void insertPointDetail(String userId, String shareId, Integer changeType, String categoryId, String ruleId, Integer point) {
         WorkKnowledgeBasePointDetail detail = new WorkKnowledgeBasePointDetail();
@@ -331,6 +335,31 @@ public class WorkKnowledgeBaseReadLikeService extends CrudService<WorkKnowledgeB
         detail.setPoint(point);
         detail.preInsert();
         pointDetailDao.insert(detail);
+
+        // 确保该用户的兑换标记记录存在(新用户首次获得积分时自动初始化)
+        ensureExchangeFlagExists(userId);
+    }
+
+    /**
+     * 确保用户的兑换标记记录存在,不存在则自动创建
+     * @param userId 用户ID
+     */
+    private void ensureExchangeFlagExists(String userId) {
+        try {
+            WorkKnowledgeBaseUserExchangeFlag flag = exchangeFlagDao.getByUserId(userId);
+            if (flag == null) {
+                flag = new WorkKnowledgeBaseUserExchangeFlag();
+                flag.setId(IdGen.uuid());
+                flag.setUserId(userId);
+                flag.setIsFirstExchanged(0);
+                flag.setDelFlag("0");
+                flag.preInsert();
+                exchangeFlagDao.insert(flag);
+            }
+        } catch (Exception e) {
+            // 标记初始化失败不影响积分发放主流程
+            e.printStackTrace();
+        }
     }
     
     /**

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

@@ -110,6 +110,9 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     @Autowired
     private WorkProjectNotifyService workProjectNotifyService;
 
+    @Autowired
+    private WorkKnowledgeBaseUserExchangeFlagDao exchangeFlagDao;
+
     /**
      * 根据文件id加载附件列表(编辑表单回显用)
      */
@@ -938,7 +941,7 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     }
 
     /**
-     * 插入一条积分明细记录
+     * 插入一条积分明细记录,同时确保该用户的兑换标记记录存在
      */
     private void insertPointDetail(String userId, String shareId, Integer changeType, String categoryId, String ruleId, Integer point) {
         WorkKnowledgeBasePointDetail detail = new WorkKnowledgeBasePointDetail();
@@ -951,6 +954,31 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         detail.setPoint(point);
         detail.preInsert();
         pointDetailDao.insert(detail);
+
+        // 确保该用户的兑换标记记录存在(新用户首次获得积分时自动初始化)
+        ensureExchangeFlagExists(userId);
+    }
+
+    /**
+     * 确保用户的兑换标记记录存在,不存在则自动创建
+     * @param userId 用户ID
+     */
+    private void ensureExchangeFlagExists(String userId) {
+        try {
+            WorkKnowledgeBaseUserExchangeFlag flag = exchangeFlagDao.getByUserId(userId);
+            if (flag == null) {
+                flag = new WorkKnowledgeBaseUserExchangeFlag();
+                flag.setId(IdGen.uuid());
+                flag.setUserId(userId);
+                flag.setIsFirstExchanged(0);
+                flag.setDelFlag("0");
+                flag.preInsert();
+                exchangeFlagDao.insert(flag);
+            }
+        } catch (Exception e) {
+            // 标记初始化失败不影响积分发放主流程
+            e.printStackTrace();
+        }
     }
 
     /**

+ 81 - 2
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBasePointController.java

@@ -5,11 +5,14 @@ package com.jeeplus.modules.WorkKnowledgeBase.web;
 
 import com.jeeplus.common.config.Global;
 import com.jeeplus.common.persistence.Page;
+import com.jeeplus.common.utils.DateUtils;
 import com.jeeplus.common.utils.StringUtils;
+import com.jeeplus.common.utils.excel.ExportExcel;
 import com.jeeplus.common.web.BaseController;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.UserPointDetail;
 import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointDetailService;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointExchangeService;
 import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeBasePointService;
 import com.jeeplus.modules.sys.entity.User;
 import com.jeeplus.modules.sys.service.OfficeService;
@@ -28,6 +31,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.List;
 
 /**
  * 知识库积分管理Controller
@@ -47,6 +51,9 @@ public class WorkKnowledgeBasePointController extends BaseController {
     @Autowired
     private OfficeService officeService;
 
+    @Autowired
+    private WorkKnowledgeBasePointExchangeService pointExchangeService;
+
     @ModelAttribute
     public WorkKnowledgeBasePoint get(@RequestParam(required=false) String id) {
         WorkKnowledgeBasePoint entity = null;
@@ -161,12 +168,84 @@ public class WorkKnowledgeBasePointController extends BaseController {
             userPointDetail.setColumnId("2");
         }
         Page<UserPointDetail> page = workKnowledgeBasePointDetailService.findUserPointDetailPage(new Page<>(request, response), userPointDetail);
-        // 查询该用户在当前分类(含子节点)下的积分总和
-        int totalPoint = workKnowledgeBasePointDetailService.sumUserPointByColumnId(userPointDetail.getUserId(), userPointDetail.getColumnId());
+        // 查询该用户在当前分类(含子节点)下的积分总和(根据积分类型筛选)
+        int totalPoint = workKnowledgeBasePointDetailService.sumUserPointByColumnId(
+            userPointDetail.getUserId(), userPointDetail.getColumnId(), userPointDetail.getPointType());
         model.addAttribute("page", page);
         model.addAttribute("userPointDetail", userPointDetail);
         model.addAttribute("totalPoint", totalPoint);
         return "modules/WorkKnowledgeBase/workKnowledgeBasePointUserDetail";
     }
 
+    /**
+     * 批量兑换积分并导出Excel
+     * @param exchangeType 兑换类型:basic-基础积分, contribution-贡献积分
+     */
+    @RequiresPermissions("workKnowledgeBase:point:listAll")
+    @RequestMapping(value = "exportExchange")
+    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) + " ==========");
+
+            // 根据兑换类型动态生成Excel列
+            String fileName = "积分兑换人员信息" + DateUtils.getDate("yyyyMMddHHmmss") + ".xlsx";
+            String[] headers;
+            if ("basic".equals(exchangeType)) {
+                // 基础积分兑换:姓名、所属部门、手机号、可兑换积分、基础积分兑换、兑换后剩余积分
+                headers = new String[]{"姓名", "所属部门", "手机号", "可兑换积分", "基础积分兑换", "兑换后剩余积分"};
+            } else {
+                // 贡献积分兑换:姓名、所属部门、手机号、可兑换积分、贡献积分兑换、兑换后剩余积分
+                headers = new String[]{"姓名", "所属部门", "手机号", "可兑换积分", "贡献积分兑换", "兑换后剩余积分"};
+            }
+            ExportExcel exportExcel = new ExportExcel("积分兑换人员信息", headers);
+
+            for (WorkKnowledgeBasePoint item : exportList) {
+                int exBasic = item.getExchangedBasicPoint() != null ? item.getExchangedBasicPoint() : 0;
+                int exContrib = item.getExchangedContributionPoint() != null ? item.getExchangedContributionPoint() : 0;
+                // 可兑换积分(兑换前的原始积分)
+                int origBasic = item.getOriginalBasicPoint() != null ? item.getOriginalBasicPoint() : 0;
+                int origContrib = item.getOriginalContributionPoint() != null ? item.getOriginalContributionPoint() : 0;
+                // 兑换后剩余积分(已从DB直接获取,因为兑换记录category_id已正确归类)
+                int remainBasic = item.getRemainingBasicPoint() != null ? item.getRemainingBasicPoint() : 0;
+                int remainContrib = item.getRemainingContributionPoint() != null ? item.getRemainingContributionPoint() : 0;
+
+                org.apache.poi.ss.usermodel.Row row = exportExcel.addRow();
+                int col = 0;
+                // 公共列:姓名、所属部门、手机号
+                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)) {
+                    // 基础积分兑换:显示可兑换积分、基础积分兑换量、兑换后剩余积分
+                    exportExcel.addCell(row, col++, origBasic, 2, Integer.class);
+                    exportExcel.addCell(row, col++, exBasic, 2, Integer.class);
+                    exportExcel.addCell(row, col++, remainBasic, 2, Integer.class);
+                } else {
+                    // 贡献积分兑换:显示可兑换积分、贡献积分兑换量、兑换后剩余积分
+                    exportExcel.addCell(row, col++, origContrib, 2, Integer.class);
+                    exportExcel.addCell(row, col++, exContrib, 2, Integer.class);
+                    exportExcel.addCell(row, col++, remainContrib, 2, Integer.class);
+                }
+            }
+
+            exportExcel.write(response, fileName).dispose();
+            return null;
+        } catch (Exception e) {
+            addMessage(redirectAttributes, "导出积分兑换人员信息失败!失败信息:" + e.getMessage());
+        }
+        return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/point/?repage";
+    }
+
 }

+ 0 - 2
src/main/java/com/jeeplus/modules/workreimbursement/web/WorkReimbursementController.java

@@ -1202,8 +1202,6 @@ public class WorkReimbursementController extends BaseController {
 
             }
 
-            System.out.println(map.get("BuyerInformationBuyerIdNum").toString());
-            System.out.println(map.get("BuyerInformationBuyerName").toString());
             //对处理后的数据根据发票号进行数据库查询,查看是否存在已报销的电子发票数据
             if ((StringUtils.isNotBlank(map.get("BuyerInformationBuyerName").toString()) && "江苏兴光项目管理有限公司".equals(map.get("BuyerInformationBuyerName").toString())) &&
                     (StringUtils.isNotBlank(map.get("BuyerInformationBuyerIdNum").toString()) && "91320000746823994F".equals(map.get("BuyerInformationBuyerIdNum").toString()))) {

+ 84 - 8
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml

@@ -149,10 +149,6 @@
             </if>
         </where>
         GROUP BY u.id, u.name, u.mobile, u.phone, o.name
-        <!-- 管理员模式下,过滤掉积分为0的记录 -->
-        <if test="isAdmin != null and isAdmin == 'admin'">
-            HAVING totalPoint > 0
-        </if>
         <choose>
             <when test="page !=null and page.orderBy != null and page.orderBy != ''">
                 ORDER BY ${page.orderBy}
@@ -212,10 +208,6 @@
                 </if>
             </where>
             GROUP BY u.id
-            <!-- 管理员模式下,过滤掉积分为0的记录 -->
-            <if test="isAdmin != null and isAdmin == 'admin'">
-                HAVING COALESCE(SUM(d.point), 0) > 0
-            </if>
         ) t
     </select>
 
@@ -265,6 +257,13 @@
             <if test="endDate != null">
                 AND d.create_date &lt;= TIMESTAMP(#{endDate}, '23:59:59')
             </if>
+            <!-- 积分类型筛选:acquire-获取(正数),exchange-兑换(负数) -->
+            <if test="pointType != null and pointType == 'acquire'">
+                AND d.point &gt; 0
+            </if>
+            <if test="pointType != null and pointType == 'exchange'">
+                AND d.point &lt; 0
+            </if>
         </where>
         <choose>
             <when test="page !=null and page.orderBy != null and page.orderBy != ''">
@@ -305,6 +304,13 @@
             <if test="endDate != null">
                 AND d.create_date &lt;= TIMESTAMP(#{endDate}, '23:59:59')
             </if>
+            <!-- 积分类型筛选:acquire-获取(正数),exchange-兑换(负数) -->
+            <if test="pointType != null and pointType == 'acquire'">
+                AND d.point &gt; 0
+            </if>
+            <if test="pointType != null and pointType == 'exchange'">
+                AND d.point &lt; 0
+            </if>
         </where>
     </select>
     
@@ -322,6 +328,26 @@
         </where>
     </select>
 
+    <!-- 根据用户ID、分类ID和积分类型统计积分总和(支持pointType筛选) -->
+    <select id="sumUserPointByColumnIdWithPointType" resultType="java.lang.Integer">
+        SELECT COALESCE(SUM(d.point), 0)
+        FROM work_knowledge_base_point_detail d
+        LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+        <where>
+            d.del_flag = '0'
+            AND d.user_id = #{userId}
+            <if test="columnId != null and columnId != ''">
+                AND (d.category_id = #{columnId} OR c.parent_ids LIKE CONCAT('%,', #{columnId}, ',%'))
+            </if>
+            <if test="pointType != null and pointType == 'acquire'">
+                AND d.point &gt; 0
+            </if>
+            <if test="pointType != null and pointType == 'exchange'">
+                AND d.point &lt; 0
+            </if>
+        </where>
+    </select>
+
     <!-- 物理删除指定条件的积分记录(用于取消点赞时删除) -->
     <delete id="deleteByUserIdAndShareIdAndChangeType">
         DELETE FROM work_knowledge_base_point_detail
@@ -331,4 +357,54 @@
           AND del_flag = '0'
     </delete>
 
+    <!-- 获取所有有积分的用户ID列表(去重) -->
+    <select id="findAllUserIdsWithPoints" resultType="java.lang.String">
+        SELECT DISTINCT user_id
+        FROM work_knowledge_base_point_detail
+        WHERE del_flag = '0'
+          AND point > 0
+    </select>
+
+    <!-- 根据分类ID获取有该分类积分的用户ID列表(去重) -->
+    <select id="findUserIdsByColumnId" resultType="java.lang.String">
+        SELECT DISTINCT d.user_id
+        FROM work_knowledge_base_point_detail d
+        LEFT 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 > 0
+          AND (d.category_id = #{columnId} OR c.parent_ids LIKE '%,' || #{columnId} || ',%')
+    </select>
+
+    <!-- 查询所有用户的基础积分和贡献积分(用于导出) -->
+    <select id="findAllUserPointSummary" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBasePoint">
+        SELECT
+            u.id AS "userId",
+            u.name AS "userName",
+            o.name AS "officeName",
+            COALESCE(u.mobile, u.phone) AS "mobile",
+            COALESCE(bp.total_bp, 0) AS "basicPoint",
+            COALESCE(cp.total_cp, 0) AS "contributionPoint"
+        FROM sys_user u
+        LEFT JOIN sys_office o ON o.id = u.office_id AND o.del_flag = '0'
+        LEFT JOIN (
+            SELECT d.user_id, SUM(d.point) AS total_bp
+            FROM work_knowledge_base_point_detail d
+            LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+            WHERE d.del_flag = '0'
+              AND (d.category_id = '1' OR c.parent_ids LIKE '%,1,%')
+            GROUP BY d.user_id
+        ) bp ON bp.user_id = u.id
+        LEFT JOIN (
+            SELECT d.user_id, SUM(d.point) AS total_cp
+            FROM work_knowledge_base_point_detail d
+            LEFT JOIN work_knowledge_point_category c ON c.id = d.category_id AND c.del_flag = '0'
+            WHERE d.del_flag = '0'
+              AND (d.category_id = '2' OR c.parent_ids LIKE '%,2,%')
+            GROUP BY d.user_id
+        ) cp ON cp.user_id = u.id
+        WHERE u.del_flag = '0'
+          AND (bp.total_bp IS NOT NULL OR cp.total_cp IS NOT NULL)
+        ORDER BY o.name, u.name
+    </select>
+
 </mapper>

+ 53 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseUserExchangeFlagDao.xml

@@ -0,0 +1,53 @@
+<?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.WorkKnowledgeBaseUserExchangeFlagDao">
+    
+    <!-- 根据用户ID获取首次兑换标记 -->
+    <select id="getByUserId" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseUserExchangeFlag">
+        SELECT
+            a.id AS "id",
+            a.user_id AS "userId",
+            a.is_first_exchanged AS "isFirstExchanged",
+            a.first_exchange_date AS "firstExchangeDate",
+            a.create_by AS "createBy.id",
+            a.create_date AS "createDate",
+            a.update_by AS "updateBy.id",
+            a.update_date AS "updateDate",
+            a.del_flag AS "delFlag"
+        FROM work_knowledge_base_user_exchange_flag a
+        WHERE a.user_id = #{userId}
+          AND a.del_flag = '0'
+    </select>
+
+    <!-- 插入兑换标记 -->
+    <insert id="insert">
+        INSERT INTO work_knowledge_base_user_exchange_flag (
+            id, user_id, is_first_exchanged, first_exchange_date,
+            create_by, create_date, update_by, update_date, del_flag
+        ) VALUES (
+            #{id}, #{userId}, #{isFirstExchanged}, #{firstExchangeDate},
+            #{createBy.id}, #{createDate}, #{updateBy.id}, #{updateDate}, #{delFlag}
+        )
+    </insert>
+
+    <!-- 更新兑换标记 -->
+    <update id="update">
+        UPDATE work_knowledge_base_user_exchange_flag SET
+            is_first_exchanged = #{isFirstExchanged},
+            first_exchange_date = #{firstExchangeDate},
+            update_by = #{updateBy.id},
+            update_date = #{updateDate}
+        WHERE id = #{id}
+    </update>
+
+    <!-- 更新用户的首次兑换状态 -->
+    <update id="updateFirstExchangeStatus">
+        UPDATE work_knowledge_base_user_exchange_flag
+        SET is_first_exchanged = #{isFirstExchanged},
+            first_exchange_date = CASE WHEN #{isFirstExchanged} = 1 THEN NOW() ELSE first_exchange_date END,
+            update_date = NOW()
+        WHERE user_id = #{userId}
+          AND del_flag = '0'
+    </update>
+
+</mapper>

+ 53 - 0
src/main/resources/mysqlDateBase/add_exchange_fields.sql

@@ -0,0 +1,53 @@
+-- ============================================
+-- 知识库积分兑换功能相关字段添加
+-- 作者:徐滕
+-- 日期:2026-06-18
+-- 说明:添加积分兑换相关字段和索引
+-- ============================================
+
+-- 1. 在 work_knowledge_base_point_detail 表添加 change_type=9 的兑换记录支持
+--    注意:change_type 字段已存在,无需新增,直接使用值 9 表示兑换扣减
+
+-- 2. 添加用户首次兑换标记表(独立管理每个用户的基础积分首次兑换状态)
+CREATE TABLE IF NOT EXISTS `work_knowledge_base_user_exchange_flag` (
+  `id` VARCHAR(64) NOT NULL COMMENT '主键ID',
+  `user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
+  `is_first_exchanged` TINYINT(1) DEFAULT 0 COMMENT '基础积分是否首次兑换:0=未兑换,1=已兑换',
+  `first_exchange_date` DATETIME DEFAULT NULL COMMENT '首次兑换时间',
+  `create_by` VARCHAR(64) DEFAULT NULL COMMENT '创建人',
+  `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_by` VARCHAR(64) DEFAULT NULL COMMENT '更新人',
+  `update_date` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `del_flag` CHAR(1) DEFAULT '0' COMMENT '删除标记',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_id` (`user_id`),
+  KEY `idx_del_flag` (`del_flag`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识库用户积分兑换标记表';
+
+-- 3. 为现有用户初始化首次兑换标记(默认都为0,未兑换过)
+INSERT INTO work_knowledge_base_user_exchange_flag (id, user_id, is_first_exchanged, create_date, update_date, del_flag)
+SELECT 
+  REPLACE(UUID(), '-', '') AS id,
+  d.user_id,
+  0 AS is_first_exchanged,
+  NOW() AS create_date,
+  NOW() AS update_date,
+  '0' AS del_flag
+FROM (SELECT DISTINCT user_id FROM work_knowledge_base_point_detail WHERE del_flag = '0') d
+WHERE NOT EXISTS (
+  SELECT 1 FROM work_knowledge_base_user_exchange_flag f 
+  WHERE f.user_id = d.user_id AND f.del_flag = '0'
+);
+
+-- 4. 确认兑换积分分类是否存在(id=9, category_name='兑换积分')
+--    如果不存在,需要手动插入或通过后台管理界面创建
+--    INSERT INTO work_knowledge_point_category (id, parent_id, parent_ids, name, sort, del_flag, create_by, create_date, update_by, update_date)
+--    VALUES ('9', '0', ',0,', '兑换积分', 90, '0', '1', NOW(), '1', NOW());
+
+-- 5. 确认兑换规则是否存在(rule_type=91/92/93)
+--    如果不存在,需要手动插入或通过后台管理界面创建
+--    INSERT INTO work_knowledge_base_point_rule (id, category_id, point_name, rule_type, point_mode, point_value, status, del_flag, create_by, create_date, update_by, update_date)
+--    VALUES 
+--      (REPLACE(UUID(), '-', ''), '9', '基础积分-首次兑换', 91, 1, 300, 1, '0', '1', NOW(), '1', NOW()),
+--      (REPLACE(UUID(), '-', ''), '9', '基础积分-非首次兑换', 92, 1, 100, 1, '0', '1', NOW(), '1', NOW()),
+--      (REPLACE(UUID(), '-', ''), '9', '基础积分-累积兑换', 93, 1, 100, 1, '0', '1', NOW(), '1', NOW());

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

@@ -170,6 +170,14 @@
 		<!-- 表格 + 分页 -->
 		<div class="full-width fl">
 			<div class="contentShadow layui-form contentDetails">
+				<div class="nav-btns">
+					<div class="layui-btn-group">
+						<shiro:hasPermission name="workKnowledgeBase:point:listAll">
+							<button type="button" data-toggle="tooltip" data-placement="top" class="layui-btn layui-btn-sm layui-bg-blue" id="exportPointUser"> 下载积分兑换人员信息</button>
+						</shiro:hasPermission>
+					</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>
@@ -186,11 +194,11 @@
             ,page: false
             ,cols: [[
                 {field:'index',align:'center', title: '序号',width:40}
-                ,{field:'userName', align: 'center', title: '经办人', minWidth: 120, templet: function(d) {
+                ,{field:'userName', align: 'center', title: '姓名', minWidth: 120, templet: function(d) {
                     var name = d.userName || '';
                     return '<a href="javascript:void(0)" class="attention-info" onclick="openUserDetailDialog(\'' + d.userId + '\',\'' + name.replace(/'/g, "\\'") + '\')">' + name + '</a>';
                 }}
-                ,{field:'officeName', align: 'center', title: '经办人部门', minWidth: 160}
+                ,{field:'officeName', align: 'center', title: '所属部门', minWidth: 160}
                 ,{field:'mobile', align: 'center', title: '手机号', minWidth: 130, templet: function(d) {
                     return d.mobile || d.phone || '';
                 }}
@@ -219,6 +227,63 @@
             ]
         });
     });
+
+    // 下载积分兑换人员信息
+    var isExporting = false; // 防止重复导出标志
+    
+    $('#exportPointUser').click(function(){
+        // 防止重复点击和重复导出
+        if (isExporting) {
+            console.log('正在导出中,请勿重复点击');
+            return false;
+        }
+        
+        var btn = $(this);
+        
+        // 从隐藏字段 columnId 获取当前选中的分类(1-基础积分,2-贡献积分)
+        var columnId = $('#columnId').val();
+        
+        isExporting = true;
+        btn.prop('disabled', true).text('兑换处理中,请稍候...');
+        
+        var randomId = "down_" + new Date().getTime();
+        var downForm = $('<form>', {
+            'action': '${ctx}/workKnowledgeBase/point/exportExchange',
+            'method': 'GET',
+            'style': 'display:none;'
+        }).appendTo('body');
+        
+        // 添加兑换类型参数:columnId=1 -> basic, columnId=2 -> contribution
+        $('<input>').attr({
+            type: 'hidden',
+            name: 'exchangeType',
+            value: columnId == '1' ? 'basic' : 'contribution'
+        }).appendTo(downForm);
+        
+        var downIframe = $('<iframe>', {'name':randomId,'style':'display:none;'}).appendTo('body');
+        downForm.attr('target', randomId);
+        
+        // 监听iframe加载完成,清理资源
+        downIframe.on('load', function(){
+            isExporting = false;
+            btn.prop('disabled', false).text(' 下载积分兑换人员信息');
+            downForm.remove();
+            downIframe.remove();
+        });
+        
+        downForm.submit();
+        
+        // 兜底:5秒后恢复按钮状态并刷新列表
+        setTimeout(function(){
+            isExporting = false;
+            btn.prop('disabled', false).text(' 下载积分兑换人员信息');
+            try { downForm.remove(); downIframe.remove(); } catch(e){}
+            // 恢复按钮后刷新列表数据
+            search();
+        }, 5000);
+
+        return false;
+    });
 </script>
 </body>
 </html>

+ 20 - 7
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBasePointUserDetail.jsp

@@ -62,14 +62,25 @@
 					<input id="pageNo" name="pageNo" type="hidden" value="${page.pageNo}"/>
 					<input id="pageSize" name="pageSize" type="hidden" value="${page.pageSize}"/>
 					<div class="commonQuery">
-						<div class="layui-item query athird">
+						<div class="layui-item query athird" style="width: 25%">
 							<label class="layui-form-label">文档名称:</label>
 							<div class="layui-input-block with-icon">
 								<input type="text" name="shareName" value="${userPointDetail.shareName}"
 								       placeholder="文档名模糊查询" class="form-control layui-input"/>
 							</div>
 						</div>
-						<div class="layui-item query athird">
+						<!-- 积分类型筛选 -->
+						<div class="layui-item query athird" style="width: 25%">
+							<label class="layui-form-label">积分类型:</label>
+							<div class="layui-input-block">
+								<select name="pointType" class="form-control layui-input">
+									<option value="">全部</option>
+									<option value="acquire" ${userPointDetail.pointType == 'acquire' ? 'selected' : ''}>积分获取</option>
+									<option value="exchange" ${userPointDetail.pointType == 'exchange' ? 'selected' : ''}>积分兑换</option>
+								</select>
+							</div>
+						</div>
+						<div class="layui-item query athird" style="width: 25%">
 							<label class="layui-form-label">获取时间:</label>
 							<div class="layui-input-block">
 								<input id="beginDate" name="beginDate" placeholder="开始时间" type="text" readonly="readonly" maxlength="20" class="laydate-icondate form-control layer-date layui-input laydate-icon query-group"
@@ -79,7 +90,7 @@
 								       value="<fmt:formatDate value="${userPointDetail.endDate}" pattern="yyyy-MM-dd"/>"/>
 							</div>
 						</div>
-						<div class="layui-item athird">
+						<div class="layui-item athird" style="width: 25%">
 							<div class="input-group">
 								<div class="layui-btn-group search-spacing">
 									<button id="searchQuery" class="layui-btn layui-btn-sm layui-bg-blue" onclick="return search()">查询</button>
@@ -132,18 +143,20 @@
 					return d.categoryName || '-';
 				}}
 				,{field:'ruleType',align:'center', title: '获取方式', width:110,templet:function(d){
+					var val = parseInt(d.point) || 0;
+					if (val < 0) return '<span class="layui-badge layui-bg-gray">积分兑换</span>';
 					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>';
-					if (rt === 91) return '<span class="layui-badge layui-bg-cyan">首次兑换</span>';
-					if (rt === 92) return '<span class="layui-badge layui-bg-gray">非首次兑换</span>';
-					if (rt === 93) return '<span class="layui-badge layui-bg-black">累积兑换</span>';
 					return '-';
 				}}
 				,{field:'point',align:'center', title: '积分额度', width:100,templet:function(d){
-					return '<span style="color:#1aa094;font-weight:bold;">+' + (d.point || 0) + '</span>';
+					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:'createDate',align:'center', title: '时间', width:170}
 			]]