Kaynağa Gözat

知识库,专家审核添加主审专家功能

徐滕 1 hafta önce
ebeveyn
işleme
809bdb918f
26 değiştirilmiş dosya ile 2276 ekleme ve 150 silme
  1. 147 0
      docs/主审专家选择功能实现说明.md
  2. 19 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseShareInfoDao.java
  3. 11 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBasePoint.java
  4. 44 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBasePointDetail.java
  5. 66 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseShareInfo.java
  6. 4 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBasePointExchangeService.java
  7. 1 0
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseReadLikeService.java
  8. 687 13
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java
  9. 261 8
      src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBaseShareController.java
  10. 107 0
      src/main/java/com/jeeplus/modules/sys/web/OfficeController.java
  11. 11 0
      src/main/java/com/jeeplus/modules/workprojectnotify/dao/WorkProjectNotifyDao.java
  12. 17 0
      src/main/java/com/jeeplus/modules/workprojectnotify/service/WorkProjectNotifyService.java
  13. 198 0
      src/main/java/com/jeeplus/modules/workprojectnotify/web/WorkProjectNotifyController.java
  14. 59 30
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml
  15. 63 4
      src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseShareInfoDao.xml
  16. 17 1
      src/main/resources/mappings/modules/workprojectnotify/WorkProjectNotifyDao.xml
  17. 37 0
      src/main/resources/mysqlDateBase/alter_work_knowledge_base_point_detail_manual_score.sql
  18. 5 0
      src/main/resources/mysqlDateBase/alter_work_knowledge_base_share_info_expert_reviewer.sql
  19. 76 0
      src/main/webapp/WEB-INF/tags/sys/selectExpert.tag
  20. 139 0
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseManualScore.jsp
  21. 124 62
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareDetail.jsp
  22. 62 21
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareForm.jsp
  23. 51 11
      src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareList.jsp
  24. 3 0
      src/main/webapp/webpage/modules/sys/sysHome.jsp
  25. 61 0
      src/main/webapp/webpage/modules/workprojectnotify/workProjectNotifyList.jsp
  26. 6 0
      src/main/webapp/webpage/modules/workprojectnotify/workProjectNotifyReadShowList.jsp

+ 147 - 0
docs/主审专家选择功能实现说明.md

@@ -0,0 +1,147 @@
+# 主审专家选择功能实现说明
+
+## 功能概述
+
+在知识库文件上传表单中,对于需要专家审核的类型(典型案例(原创)、技术总结、培训心得),允许创建人选择1-2位专家组成员作为主审人。
+
+## 实现内容
+
+### 1. Tag 标签文件
+
+**文件路径**: `src/main/webapp/WEB-INF/tags/sys/selectExpert.tag`
+
+创建了新的 tag 文件 `selectExpert.tag`,用于选择专家组成员:
+- 支持多选(复选框模式)
+- 限制最多选择2位专家(可通过 `maxSelect` 参数自定义)
+- 调用 `/sys/office/treeDataAll?type=3&isExpert=1` 接口获取专家列表
+- 前端验证:至少选择1位,最多选择2位
+- 显示格式:专家姓名用逗号分隔
+
+**使用示例**:
+```jsp
+<sys:selectExpert id="expertReviewer" name="expertReviewerIds" 
+                  value="${entity.expertReviewerIds}" 
+                  labelName="expertReviewerNames" 
+                  labelValue="${entity.expertReviewerNames}"
+                  title="专家" 
+                  cssClass="form-control judgment layui-input" 
+                  allowClear="true" 
+                  maxSelect="2"/>
+```
+
+### 2. 后端接口修改
+
+#### 2.1 OfficeController.java
+
+**文件路径**: `src/main/java/com/jeeplus/modules/sys/web/OfficeController.java`
+
+**修改内容**:
+1. 注入 `WorkKnowledgeExpertService`
+2. 在 `treeDataAll` 方法中添加 `isExpert` 参数
+3. 当 `type=3` 且 `isExpert=1` 时,过滤只返回专家用户所在的部门
+
+**核心逻辑**:
+```java
+if("3".equals(type) && "1".equals(isExpert)){
+    // 获取所有启用的专家
+    List<WorkKnowledgeExpert> expertList = expertService.findEnabledList();
+    // 过滤mapList,只保留有专家的部门
+    ...
+}
+```
+
+### 3. 实体类修改
+
+#### 3.1 WorkKnowledgeBaseShareInfo.java
+
+**文件路径**: `src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseShareInfo.java`
+
+**新增字段**:
+```java
+/** 主审专家ID列表(多个用逗号分隔) */
+private String expertReviewerIds;
+
+/** 主审专家姓名列表(多个用逗号分隔,非数据库字段,用于展示) */
+private String expertReviewerNames;
+```
+
+包含 getter/setter 方法。
+
+### 4. 表单页面修改
+
+**文件路径**: `src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareForm.jsp`
+
+**修改内容**:
+1. 在 Controller 的 `form` 方法中添加 `treeName` 传递
+2. 在表单中添加主审专家选择器,条件显示:
+   - 非问答答疑分类
+   - 且(技术总结 或 培训心得 或 典型案例+原创)
+
+**显示条件**:
+```jsp
+<c:if test="${isQaCategory == false && (treeName == '技术总结' || treeName == '培训心得' || (treeName == '典型案例' && entity.contentAttribute == '0'))}">
+```
+
+**UI 提示**:
+```html
+<span class="help-inline" style="color: #999; font-size: 12px;">可选可不选,最多选择2位专家</span>
+```
+
+### 5. MyBatis Mapper 修改
+
+**文件路径**: `src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseShareInfoDao.xml`
+
+**修改内容**:
+1. `shareColumns` SQL 片段添加 `expert_reviewer_ids` 字段映射
+2. `findDynamicList` SELECT 子句添加字段
+3. `findDynamicList` GROUP BY 子句添加字段
+4. `insert` 语句添加字段
+5. `update` 语句添加字段
+
+### 6. 数据库迁移脚本
+
+**文件路径**: `src/main/resources/mysqlDateBase/alter_work_knowledge_base_share_info_expert_reviewer.sql`
+
+**SQL 内容**:
+```sql
+ALTER TABLE work_knowledge_base_share_info 
+ADD COLUMN expert_reviewer_ids VARCHAR(500) COMMENT '主审专家ID列表(多个用逗号分隔)' AFTER manual_score_time;
+```
+
+**执行说明**: 需要在数据库中手动执行此脚本以添加新字段。
+
+## 数据流程
+
+1. **前端展示**: 用户打开表单 → Controller 查询 treeName → JSP 根据条件显示主审专家选择器
+2. **选择专家**: 点击搜索按钮 → 调用 `/sys/office/treeDataAll?type=3&isExpert=1` → 返回专家所在部门列表 → 用户勾选专家
+3. **提交保存**: 表单提交 → `expertReviewerIds` 字段保存到数据库(格式:`id1,id2`)
+4. **编辑回显**: 从数据库读取 `expertReviewerIds` → 通过 UserUtils 获取专家姓名 → 填充到 `expertReviewerNames`
+
+## 注意事项
+
+1. **专家定义**: 专家是在 `work_knowledge_expert` 表中配置且状态为启用的用户
+2. **可选性**: 主审专家是可选的,不强制要求选择
+3. **数量限制**: 最多选择2位专家,前端和 tag 文件中都有验证
+4. **数据存储**: 只存储专家ID列表(逗号分隔),姓名为动态获取(非持久化字段)
+5. **权限控制**: 只有启用的专家才会出现在选择列表中
+
+## 相关文件清单
+
+| 文件 | 类型 | 说明 |
+|------|------|------|
+| `selectExpert.tag` | 新建 | 专家选择器 tag |
+| `OfficeController.java` | 修改 | 添加 isExpert 参数支持 |
+| `WorkKnowledgeBaseShareInfo.java` | 修改 | 添加主审专家字段 |
+| `WorkKnowledgeBaseShareController.java` | 修改 | form 方法添加 treeName |
+| `workKnowledgeBaseShareForm.jsp` | 修改 | 添加主审专家选择器 UI |
+| `WorkKnowledgeBaseShareInfoDao.xml` | 修改 | 添加字段映射 |
+| `alter_work_knowledge_base_share_info_expert_reviewer.sql` | 新建 | 数据库迁移脚本 |
+
+## 测试建议
+
+1. 测试不同分类下主审专家选择器的显示/隐藏逻辑
+2. 测试选择1位、2位专家的正常保存
+3. 测试选择超过2位专家的拦截提示
+4. 测试编辑时的回显功能
+5. 测试清空主审专家的功能
+6. 测试数据库中 expert_reviewer_ids 字段的正确存储

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

@@ -56,4 +56,23 @@ public interface WorkKnowledgeBaseShareInfoDao extends CrudDao<WorkKnowledgeBase
      * @param entity 包含 id、treeNodeId、updateBy、updateDate
      */
     int updateCategory(WorkKnowledgeBaseShareInfo entity);
+
+    /**
+     * 手动赋分:更新审核状态为通过 + 写入手动赋分字段
+     * @param entity 包含 id、审核状态、手动赋分四个字段、updateBy、updateDate
+     */
+    int updateManualScore(WorkKnowledgeBaseShareInfo entity);
+
+    /**
+     * 清除手动赋分标记
+     * @param id 文件ID
+     */
+    int clearManualScore(@org.apache.ibatis.annotations.Param("id") String id);
+
+    /**
+     * 根据ID查询并加行锁(SELECT ... FOR UPDATE),用于审核操作的并发控制
+     * @param id 文件ID
+     * @return 文件信息(已锁定)
+     */
+    WorkKnowledgeBaseShareInfo getForUpdate(@org.apache.ibatis.annotations.Param("id") String id);
 }

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

@@ -91,6 +91,9 @@ public class WorkKnowledgeBasePoint extends DataEntity<WorkKnowledgeBasePoint> {
 
 	/** 兑换积分额度(绝对值) */
 	private Integer exchangePoint;
+
+	/** 是否手动赋分筛选(0否 1是,仅用于查询条件) */
+	private String isManualScore;
 	
 	public WorkKnowledgeBasePoint() {
 		super();
@@ -379,4 +382,12 @@ public class WorkKnowledgeBasePoint extends DataEntity<WorkKnowledgeBasePoint> {
 	public void setExchangePoint(Integer exchangePoint) {
 		this.exchangePoint = exchangePoint;
 	}
+
+	public String getIsManualScore() {
+		return isManualScore;
+	}
+
+	public void setIsManualScore(String isManualScore) {
+		this.isManualScore = isManualScore;
+	}
 }

+ 44 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBasePointDetail.java

@@ -28,6 +28,18 @@ public class WorkKnowledgeBasePointDetail extends DataEntity<WorkKnowledgeBasePo
     /** 积分值 */
     private Integer point;
 
+    /** 是否手动赋分(0否 1是) */
+    private String isManualScore;
+
+    /** 赋分人ID(仅手动赋分时填写) */
+    private String manualScoreUserId;
+
+    /** 被赋分人ID(仅手动赋分时填写,与userId相同) */
+    private String manualScoreTargetUserId;
+
+    /** 赋分时间(仅手动赋分时填写) */
+    private java.util.Date manualScoreTime;
+
     public String getUserId() {
         return userId;
     }
@@ -75,4 +87,36 @@ public class WorkKnowledgeBasePointDetail extends DataEntity<WorkKnowledgeBasePo
     public void setPoint(Integer point) {
         this.point = point;
     }
+
+    public String getIsManualScore() {
+        return isManualScore;
+    }
+
+    public void setIsManualScore(String isManualScore) {
+        this.isManualScore = isManualScore;
+    }
+
+    public String getManualScoreUserId() {
+        return manualScoreUserId;
+    }
+
+    public void setManualScoreUserId(String manualScoreUserId) {
+        this.manualScoreUserId = manualScoreUserId;
+    }
+
+    public String getManualScoreTargetUserId() {
+        return manualScoreTargetUserId;
+    }
+
+    public void setManualScoreTargetUserId(String manualScoreTargetUserId) {
+        this.manualScoreTargetUserId = manualScoreTargetUserId;
+    }
+
+    public java.util.Date getManualScoreTime() {
+        return manualScoreTime;
+    }
+
+    public void setManualScoreTime(java.util.Date manualScoreTime) {
+        this.manualScoreTime = manualScoreTime;
+    }
 }

+ 66 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseShareInfo.java

@@ -71,6 +71,24 @@ public class WorkKnowledgeBaseShareInfo extends DataEntity<WorkKnowledgeBaseShar
     /** 点击量(每次点击+1,不去重) */
     private Integer clickCount;
 
+    /** 是否手动赋分(0否 1是) */
+    private String isManualScore;
+
+    /** 赋分人ID */
+    private String manualScoreUserId;
+
+    /** 被赋分人ID */
+    private String manualScoreTargetUserId;
+
+    /** 赋分时间 */
+    private Date manualScoreTime;
+
+    /** 主审专家ID列表(多个用逗号分隔) */
+    private String expertReviewerIds;
+
+    /** 主审专家姓名列表(多个用逗号分隔,非数据库字段,用于展示) */
+    private String expertReviewerNames;
+
     public String getTreeNodeId() {
         return treeNodeId;
     }
@@ -262,4 +280,52 @@ public class WorkKnowledgeBaseShareInfo extends DataEntity<WorkKnowledgeBaseShar
     public void setClickCount(Integer clickCount) {
         this.clickCount = clickCount;
     }
+
+    public String getIsManualScore() {
+        return isManualScore;
+    }
+
+    public void setIsManualScore(String isManualScore) {
+        this.isManualScore = isManualScore;
+    }
+
+    public String getManualScoreUserId() {
+        return manualScoreUserId;
+    }
+
+    public void setManualScoreUserId(String manualScoreUserId) {
+        this.manualScoreUserId = manualScoreUserId;
+    }
+
+    public String getManualScoreTargetUserId() {
+        return manualScoreTargetUserId;
+    }
+
+    public void setManualScoreTargetUserId(String manualScoreTargetUserId) {
+        this.manualScoreTargetUserId = manualScoreTargetUserId;
+    }
+
+    public Date getManualScoreTime() {
+        return manualScoreTime;
+    }
+
+    public void setManualScoreTime(Date manualScoreTime) {
+        this.manualScoreTime = manualScoreTime;
+    }
+
+    public String getExpertReviewerIds() {
+        return expertReviewerIds;
+    }
+
+    public void setExpertReviewerIds(String expertReviewerIds) {
+        this.expertReviewerIds = expertReviewerIds;
+    }
+
+    public String getExpertReviewerNames() {
+        return expertReviewerNames;
+    }
+
+    public void setExpertReviewerNames(String expertReviewerNames) {
+        this.expertReviewerNames = expertReviewerNames;
+    }
 }

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

@@ -124,6 +124,7 @@ public class WorkKnowledgeBasePointExchangeService extends CrudService<WorkKnowl
         detail.setChangeType(9); // 9表示兑换扣减
         detail.setCategoryId("1"); // 基础积分L1分类ID,使扣减自然计入基础积分的已兑换列
         detail.setPoint(-exchangeablePoints); // 负数表示扣减
+        detail.setIsManualScore("0");
         detail.setDelFlag("0");
         detail.preInsert();
         pointDetailDao.insert(detail);
@@ -157,6 +158,7 @@ public class WorkKnowledgeBasePointExchangeService extends CrudService<WorkKnowl
         detail.setChangeType(9); // 9表示兑换扣减
         detail.setCategoryId("2"); // 贡献积分L1分类ID,使扣减自然计入贡献积分的已兑换列
         detail.setPoint(-contributionPoints); // 负数表示扣减,全额清零
+        detail.setIsManualScore("0");
         detail.setDelFlag("0");
         detail.preInsert();
         pointDetailDao.insert(detail);
@@ -301,6 +303,7 @@ public class WorkKnowledgeBasePointExchangeService extends CrudService<WorkKnowl
                             detail.setChangeType(9);
                             detail.setCategoryId("1"); // 基础积分分类,使扣减自然计入基础积分汇总
                             detail.setPoint(-basicExchanged);
+                            detail.setIsManualScore("0");
                             detail.setDelFlag("0");
                             detail.preInsert();
                             pointDetailDao.insert(detail);
@@ -329,6 +332,7 @@ public class WorkKnowledgeBasePointExchangeService extends CrudService<WorkKnowl
                         cpDetail.setChangeType(9);
                         cpDetail.setCategoryId("2"); // 贡献积分分类,使扣减自然计入贡献积分汇总
                         cpDetail.setPoint(-contribExchanged);
+                        cpDetail.setIsManualScore("0");
                         cpDetail.setDelFlag("0");
                         cpDetail.preInsert();
                         pointDetailDao.insert(cpDetail);

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

@@ -363,6 +363,7 @@ public class WorkKnowledgeBaseReadLikeService extends CrudService<WorkKnowledgeB
         detail.setCategoryId(categoryId);
         detail.setRuleId(ruleId);
         detail.setPoint(point);
+        detail.setIsManualScore("0");
         detail.preInsert();
         pointDetailDao.insert(detail);
 

+ 687 - 13
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/service/WorkKnowledgeBaseShareService.java

@@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -66,6 +67,8 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     private static final Integer POINT_CHANGE_CREATE = 1;
     /** 积分明细变动类型:审核加分 */
     private static final Integer POINT_CHANGE_AUDIT = 2;
+    /** 积分明细变动类型:手动赋分加分 */
+    private static final Integer POINT_CHANGE_MANUAL = 3;
     /** 问答积分规则类型:提问积分 */
     private static final String QA_RULE_TYPE_QUESTION = "1";
     /** 问答积分规则类型:回答积分 */
@@ -380,10 +383,12 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         boolean isQaCategory = false;
         boolean isTypicalCase = false; // 是否为典型案例分类
         boolean isInternalDocRootNode = false; // 是否为"公司内部发文"根节点(parent_id=0)
+        String treeNameForNotify = null; // 保存treeName,用于后续通知判断
         
         if (StringUtils.isNotBlank(shareInfo.getTreeNodeId())) {
             WorkKnowledgeBaseTreeInfo treeInfo = getTreeInfoById(shareInfo.getTreeNodeId());
             if (treeInfo != null) {
+                treeNameForNotify = treeInfo.getTreeName();
                 if ("问答答疑".equals(treeInfo.getTreeName())) {
                     isQaCategory = true;
                 }
@@ -494,6 +499,145 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
                 dynamicValueInfoDao.insert(valueInfo);
             }
         }
+
+        // ========== 仅针对典型案例(原创)、培训心得、技术总结 才发送代办/通知 ==========
+        boolean needsNotify = "技术总结".equals(treeNameForNotify) || "培训心得".equals(treeNameForNotify)
+                || ("典型案例".equals(treeNameForNotify) && "0".equals(shareInfo.getContentAttribute()));
+        
+        if (needsNotify) {
+            //在往work_project_notify表中添加代办或者通知数据之前 需要根据shareInfo.getId()(即WorkProjectNotify表中的notify_id字段)进行判断,如果已经存在数据,则需要全部删除之后重新添加,否则可能导致重复数据
+            WorkProjectNotify deleteEntity = new WorkProjectNotify();
+            deleteEntity.setNotifyId(shareInfo.getId());
+            workProjectNotifyService.deleteByNotifyId(deleteEntity);
+
+            // 获取模块名称(根节点名)
+            String moduleName = "";
+            if (StringUtils.isNotBlank(shareInfo.getRootId())) {
+                WorkKnowledgeBaseTreeInfo rootTreeInfo = getTreeInfoById(shareInfo.getRootId());
+                if (rootTreeInfo != null && StringUtils.isNotBlank(rootTreeInfo.getTreeName())) {
+                    moduleName = rootTreeInfo.getTreeName();
+                }
+            }
+
+            // ========== 主审专家信息处理 ==========
+            String expertReviewerIds = shareInfo.getExpertReviewerIds();
+            String[] expertIdArr = null;
+            if (StringUtils.isNotBlank(expertReviewerIds)) {
+                expertIdArr = expertReviewerIds.split(",");
+                String expertReviewerNames = shareInfo.getExpertReviewerNames();
+                String[] expertNameArr = (StringUtils.isNotBlank(expertReviewerNames)) ? expertReviewerNames.split(",") : new String[0];
+                for (int i = 0; i < expertIdArr.length; i++) {
+                    String expertId = expertIdArr[i].trim();
+                    String expertName = (i < expertNameArr.length) ? expertNameArr[i].trim() : "";
+                    if (StringUtils.isBlank(expertId)) {
+                        continue;
+                    }
+                    // 确定所属人:如果submit_audit_user_id为空,则为create_by;否则为submit_audit_user_id
+                    String ownerUserId = StringUtils.isNotBlank(shareInfo.getSubmitAuditUserId())
+                            ? shareInfo.getSubmitAuditUserId()
+                            : (shareInfo.getCreateBy() != null ? shareInfo.getCreateBy().getId() : "");
+                    User ownerUser = UserUtils.get(ownerUserId);
+                    String ownerName = (ownerUser != null && StringUtils.isNotBlank(ownerUser.getName()))
+                            ? ownerUser.getName()
+                            : "用户";
+                    String notifyStr = "创建人:" + ownerName + "在【" + moduleName + "】模块中创建了一个名为【" + shareInfo.getName() + "】的知识库文件,需要审核";
+                    String titleStr = "创建人:" + ownerName + "在【" + moduleName + "】模块中创建了一个名为【" + shareInfo.getName() + "】的知识库文件,需要审核";
+
+                    User user = UserUtils.get(expertId);
+
+                    // 向主审专家发送代办
+                    if (StringUtils.isNotBlank(user.getId())) {
+                        workProjectNotifyService
+                                .save(UtilNotify.saveNotify(shareInfo.getId(),
+                                        user,
+                                        user.getCompany().getId(),
+                                        titleStr,
+                                        notifyStr,
+                                        "200",
+                                        "0",
+                                        "待审批",
+                                        ""));
+                    }
+
+                }
+            }
+            
+            // 通知剩余专家(排除主审专家);如果没有主审专家,则通知所有专家
+            notifyRemainingExperts(shareInfo, expertIdArr, moduleName);
+        }
+    }
+
+    /**
+     * 通知剩余专家(排除主审专家)
+     * @param shareInfo 知识库文件信息
+     * @param mainExpertIds 主审专家ID数组
+     * @param moduleName 模块名称(根节点名)
+     */
+    private void notifyRemainingExperts(WorkKnowledgeBaseShareInfo shareInfo, String[] mainExpertIds, String moduleName) {
+        // 构建主审专家ID集合,用于排除
+        Set<String> mainExpertIdSet = new HashSet<>();
+        if (mainExpertIds != null) {
+            for (String expertId : mainExpertIds) {
+                if (StringUtils.isNotBlank(expertId)) {
+                    mainExpertIdSet.add(expertId.trim());
+                }
+            }
+        }
+
+        // 查询所有启用的专家列表
+        List<WorkKnowledgeExpert> allEnabledExperts = expertService.findEnabledList();
+        if (allEnabledExperts == null || allEnabledExperts.isEmpty()) {
+            return;
+        }
+
+        // 过滤出非主审专家的剩余专家
+        List<WorkKnowledgeExpert> remainingExperts = new ArrayList<>();
+        for (WorkKnowledgeExpert expert : allEnabledExperts) {
+            if (!mainExpertIdSet.contains(expert.getUserId())) {
+                remainingExperts.add(expert);
+            }
+        }
+
+        // 如果没有剩余专家,直接返回
+        if (remainingExperts.isEmpty()) {
+            return;
+        }
+
+        // 对剩余专家发送通知
+        // 确定所属人:如果submit_audit_user_id为空,则为create_by;否则为submit_audit_user_id
+        String ownerUserId = StringUtils.isNotBlank(shareInfo.getSubmitAuditUserId()) 
+                ? shareInfo.getSubmitAuditUserId() 
+                : (shareInfo.getCreateBy() != null ? shareInfo.getCreateBy().getId() : "");
+        User ownerUser = UserUtils.get(ownerUserId);
+        String ownerName = (ownerUser != null && StringUtils.isNotBlank(ownerUser.getName())) 
+                ? ownerUser.getName() 
+                : "用户";
+        String notifyStr = "创建人:" + ownerName + "在【" + moduleName + "】模块中创建了一个名为【" + shareInfo.getName() + "】的知识库文件,需要审核。您可登录知识库进行审核";
+        String titleStr = "创建人:" + ownerName + "在【" + moduleName + "】模块中创建了一个名为【" + shareInfo.getName() + "】的知识库文件,需要审核。您可登录知识库进行审核";
+
+        for (WorkKnowledgeExpert expert : remainingExperts) {
+            User user = UserUtils.get(expert.getUserId());
+            if (user == null || StringUtils.isBlank(user.getId())) {
+                continue;
+            }
+
+            try {
+                workProjectNotifyService.save(UtilNotify.saveNotify(
+                        shareInfo.getId(),
+                        user,
+                        user.getCompany().getId(),
+                        titleStr,
+                        notifyStr,
+                        "200",
+                        "0",
+                        "待通知",
+                        ""
+                ));
+            } catch (Exception e) {
+                // 记录异常但不中断其他专家的通知发送
+                e.printStackTrace();
+            }
+        }
     }
 
     /**
@@ -564,7 +708,7 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         if (entity == null) {
             throw new RuntimeException("文件记录不存在");
         }
-        if (entity.getAuditStatus() == null || entity.getAuditStatus() != AUDIT_STATUS_PENDING) {
+        if (entity.getAuditStatus() == null || !AUDIT_STATUS_PENDING.equals(entity.getAuditStatus())) {
             throw new RuntimeException("当前状态不允许发起审核");
         }
         entity.setAuditStatus(AUDIT_STATUS_AUDITING);
@@ -582,11 +726,12 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
      */
     @Transactional(readOnly = false)
     public void auditPass(String fileId, String auditUserId, String auditOpinion, Integer score) {
-        WorkKnowledgeBaseShareInfo entity = dao.get(fileId);
+        // 使用 SELECT ... FOR UPDATE 加行锁,防止并发审核
+        WorkKnowledgeBaseShareInfo entity = dao.getForUpdate(fileId);
         if (entity == null) {
             throw new RuntimeException("文件记录不存在");
         }
-        if (entity.getAuditStatus() == null || entity.getAuditStatus() == AUDIT_STATUS_REJECTED || entity.getAuditStatus() == AUDIT_STATUS_PASSED) {
+        if (entity.getAuditStatus() == null || AUDIT_STATUS_REJECTED.equals(entity.getAuditStatus()) || AUDIT_STATUS_PASSED.equals(entity.getAuditStatus())) {
             throw new RuntimeException("当前状态不允许审核");
         }
         // 禁止审核自己提交的文件
@@ -603,15 +748,17 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         boolean needsScore = false;
         boolean isTypicalCaseOriginal = false; // 典型案例(原创)
         boolean isTypicalCaseReprint = false;  // 典型案例(转载)
+        boolean needsExpertAudit = false; // 是否需要专家审核(技术总结/培训心得/典型案例原创)
         
         if (StringUtils.isNotBlank(entity.getTreeNodeId())) {
             WorkKnowledgeBaseTreeInfo treeInfo = getTreeInfoById(entity.getTreeNodeId());
             if (treeInfo != null) {
                 String treeName = treeInfo.getTreeName();
                 
-                // 技术总结/培训心得:需要评分
+                // 技术总结/培训心得:需要评分,需要专家审核
                 if ("技术总结".equals(treeName) || "培训心得".equals(treeName)) {
                     needsScore = true;
+                    needsExpertAudit = true;
                     // 验证评分必填
                     if (score == null) {
                         throw new RuntimeException("请输入评分分数");
@@ -624,6 +771,7 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
                         // 典型案例(原创):需要专家审核,使用评分机制
                         isTypicalCaseOriginal = true;
                         needsScore = true;
+                        needsExpertAudit = true;
                         // 校验当前用户是否为专家
                         boolean isExpert = expertService.isUserBound(auditUserId);
                         if (!isExpert) {
@@ -640,6 +788,41 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
             }
         }
         
+        // ========== 主审专家抢办名额验证(仅对需要专家审核的分类生效) ==========
+        if (needsExpertAudit) {
+            String expertReviewerIds = entity.getExpertReviewerIds();
+            if (StringUtils.isNotBlank(expertReviewerIds)) {
+                Set<String> mainExpertIdSet = new HashSet<>(Arrays.asList(expertReviewerIds.split(",")));
+                boolean isMainExpert = mainExpertIdSet.contains(auditUserId);
+                if (!isMainExpert) {
+                    // 非主审专家:检查是否还有剩余抢办名额
+                    int mainExpertCount = 0;
+                    for (String eid : mainExpertIdSet) {
+                        if (StringUtils.isNotBlank(eid.trim())) {
+                            mainExpertCount++;
+                        }
+                    }
+                    int nonMainSlots = AUDIT_PASS_REQUIRED - mainExpertCount;
+                    if (nonMainSlots <= 0) {
+                        throw new RuntimeException("主审专家名额已满,非指定专家无法审核");
+                    }
+                    // 统计本轮非主审专家已审核人数
+                    int nonMainAuditedCount = 0;
+                    List<WorkKnowledgeBaseAuditRecord> currentRecords = auditRecordDao.findByFileIdSinceTime(fileId, startTime);
+                    if (currentRecords != null) {
+                        for (WorkKnowledgeBaseAuditRecord rec : currentRecords) {
+                            if (!mainExpertIdSet.contains(rec.getAuditUserId())) {
+                                nonMainAuditedCount++;
+                            }
+                        }
+                    }
+                    if (nonMainAuditedCount >= nonMainSlots) {
+                        throw new RuntimeException("非主审专家审核名额已满,无法审核");
+                    }
+                }
+            }
+        }
+        
         // 保存审核记录
         WorkKnowledgeBaseAuditRecord record = new WorkKnowledgeBaseAuditRecord();
         record.setFileId(fileId);
@@ -681,6 +864,69 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
             entity.preUpdate();
             dao.updateAuditStatus(entity);
         }
+
+        //此处是审核通过,查询审核人是否存在代办,如果存在,则将审核人的代办进行逻辑删除
+        WorkProjectNotify notify = workProjectNotifyService.getByNotifyIdAndNotifyUser(entity.getId(),auditUserId);// 查询该条数据是否存在为审核的代办和通知
+        if (notify != null) {
+            notify.setAuditor(auditUserId);
+            notify.setStatus("1");
+            workProjectNotifyService.updateNewReadStateByNotifyId(notify);
+        }
+
+        // ========== 非主审专家审核完最后一席后,清理剩余非主审专家的通知 ==========
+        if (needsExpertAudit) {
+            String expertReviewerIds = entity.getExpertReviewerIds();
+            if (StringUtils.isNotBlank(expertReviewerIds)) {
+                Set<String> mainExpertIdSet = new HashSet<>(Arrays.asList(expertReviewerIds.split(",")));
+                boolean isMainExpert = mainExpertIdSet.contains(auditUserId);
+                if (!isMainExpert) {
+                    // 统计主审专家人数
+                    int mainExpertCount = 0;
+                    for (String eid : mainExpertIdSet) {
+                        if (StringUtils.isNotBlank(eid.trim())) {
+                            mainExpertCount++;
+                        }
+                    }
+                    int nonMainSlots = AUDIT_PASS_REQUIRED - mainExpertCount;
+                    // 统计本轮非主审专家已通过人数(包含当前审核人),并收集已审核的非主审专家ID
+                    int nonMainPassCount = 0;
+                    Set<String> auditedNonMainIds = new HashSet<>();
+                    List<WorkKnowledgeBaseAuditRecord> currentRecords = auditRecordDao.findByFileIdSinceTime(fileId, startTime);
+                    if (currentRecords != null) {
+                        for (WorkKnowledgeBaseAuditRecord rec : currentRecords) {
+                            if (!mainExpertIdSet.contains(rec.getAuditUserId())) {
+                                auditedNonMainIds.add(rec.getAuditUserId());
+                                if (rec.getAuditResult() != null && rec.getAuditResult() == 1) {
+                                    nonMainPassCount++;
+                                }
+                            }
+                        }
+                    }
+                    // 非主审专家席位已满,筛选出还未审核的非主审专家
+                    if (nonMainPassCount >= nonMainSlots) {
+                        // 查询所有启用的专家,筛选出非主审且未审核的专家
+                        List<WorkKnowledgeExpert> allEnabledExperts = expertService.findEnabledList();
+                        if (allEnabledExperts != null) {
+                            for (WorkKnowledgeExpert expert : allEnabledExperts) {
+                                String expertUid = expert.getUserId();
+                                // 排除主审专家、排除已审核的非主审专家
+                                if (!mainExpertIdSet.contains(expertUid)
+                                        && !auditedNonMainIds.contains(expertUid)
+                                        && StringUtils.isNotBlank(expertUid)) {
+                                    // 对剩余未审核的非主审专家进行通知逻辑删除(按用户精确删除,不影响主审专家通知)
+                                    WorkProjectNotify disposenotify = new WorkProjectNotify();
+                                    disposenotify.setNotifyId(entity.getId());
+                                    User expertUser = new User();
+                                    expertUser.setId(expertUid);
+                                    disposenotify.setUser(expertUser);
+                                    workProjectNotifyService.modifyDelflagByUser(disposenotify);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
     }
 
     /**
@@ -689,11 +935,12 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
      */
     @Transactional(readOnly = false)
     public void auditReject(String fileId, String auditUserId, String auditOpinion) {
-        WorkKnowledgeBaseShareInfo entity = dao.get(fileId);
+        // 使用 SELECT ... FOR UPDATE 加行锁,防止并发审核
+        WorkKnowledgeBaseShareInfo entity = dao.getForUpdate(fileId);
         if (entity == null) {
             throw new RuntimeException("文件记录不存在");
         }
-        if (entity.getAuditStatus() == null || entity.getAuditStatus() == AUDIT_STATUS_REJECTED || entity.getAuditStatus() == AUDIT_STATUS_PASSED) {
+        if (entity.getAuditStatus() == null || AUDIT_STATUS_REJECTED.equals(entity.getAuditStatus()) || AUDIT_STATUS_PASSED.equals(entity.getAuditStatus())) {
             throw new RuntimeException("当前状态不允许审核");
         }
         // 禁止审核自己提交的文件
@@ -705,6 +952,51 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         if (auditRecordDao.countByFileIdAndUserIdSinceTime(fileId, auditUserId, startTime) > 0) {
             throw new RuntimeException("您已审核过该文件,不可重复审核");
         }
+        
+        // ========== 主审专家抢办名额验证(仅对需要专家审核的分类生效) ==========
+        boolean needsExpertAudit = false;
+        if (StringUtils.isNotBlank(entity.getTreeNodeId())) {
+            WorkKnowledgeBaseTreeInfo treeInfo = getTreeInfoById(entity.getTreeNodeId());
+            if (treeInfo != null) {
+                String treeName = treeInfo.getTreeName();
+                needsExpertAudit = "技术总结".equals(treeName) || "培训心得".equals(treeName)
+                        || ("典型案例".equals(treeName) && "0".equals(entity.getContentAttribute()));
+            }
+        }
+        if (needsExpertAudit) {
+            String expertReviewerIds = entity.getExpertReviewerIds();
+            if (StringUtils.isNotBlank(expertReviewerIds)) {
+                Set<String> mainExpertIdSet = new HashSet<>(Arrays.asList(expertReviewerIds.split(",")));
+                boolean isMainExpert = mainExpertIdSet.contains(auditUserId);
+                if (!isMainExpert) {
+                    // 非主审专家:检查是否还有剩余抢办名额
+                    int mainExpertCount = 0;
+                    for (String eid : mainExpertIdSet) {
+                        if (StringUtils.isNotBlank(eid.trim())) {
+                            mainExpertCount++;
+                        }
+                    }
+                    int nonMainSlots = AUDIT_PASS_REQUIRED - mainExpertCount;
+                    if (nonMainSlots <= 0) {
+                        throw new RuntimeException("主审专家名额已满,非指定专家无法审核");
+                    }
+                    // 统计本轮非主审专家已审核人数
+                    int nonMainAuditedCount = 0;
+                    List<WorkKnowledgeBaseAuditRecord> currentRecords = auditRecordDao.findByFileIdSinceTime(fileId, startTime);
+                    if (currentRecords != null) {
+                        for (WorkKnowledgeBaseAuditRecord rec : currentRecords) {
+                            if (!mainExpertIdSet.contains(rec.getAuditUserId())) {
+                                nonMainAuditedCount++;
+                            }
+                        }
+                    }
+                    if (nonMainAuditedCount >= nonMainSlots) {
+                        throw new RuntimeException("非主审专家审核名额已满,无法审核");
+                    }
+                }
+            }
+        }
+        
         // 保存审核记录
         WorkKnowledgeBaseAuditRecord record = new WorkKnowledgeBaseAuditRecord();
         record.setFileId(fileId);
@@ -722,6 +1014,17 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         entity.setAuditRejectCount(auditRecordDao.countRejectByFileIdSinceTime(fileId, startTime));
         entity.preUpdate();
         dao.updateAuditStatus(entity);
+
+        //此处是驳回,则需要将所有的通知和代办都给逻辑删除
+        List<WorkProjectNotify> listByNotifyId = workProjectNotifyService.getListByNotifyId(entity.getId());// 查询该条数据是否存在为审核的代办和通知
+        if (listByNotifyId != null && !listByNotifyId.isEmpty()) {
+            //循环 对这些数据进行逻辑删除
+            for (WorkProjectNotify notify : listByNotifyId) {
+                notify.setDelFlag(WorkProjectNotify.DEL_FLAG_DELETE);
+                workProjectNotifyService.modifyDelflag(notify);
+            }
+        }
+
     }
 
     /**
@@ -734,7 +1037,7 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         if (entity == null) {
             throw new RuntimeException("文件记录不存在");
         }
-        if (entity.getAuditStatus() == null || entity.getAuditStatus() == AUDIT_STATUS_REJECTED || entity.getAuditStatus() == AUDIT_STATUS_PASSED) {
+        if (entity.getAuditStatus() == null || AUDIT_STATUS_REJECTED.equals(entity.getAuditStatus()) || AUDIT_STATUS_PASSED.equals(entity.getAuditStatus())) {
             throw new RuntimeException("当前状态不允许撤回");
         }
         // 校验权限:仅提交审核人或管理员可撤回
@@ -748,6 +1051,17 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         entity.setAuditRejectCount(0);
         entity.preUpdate();
         dao.updateAuditStatus(entity);
+
+        //撤回操作,需要到work_project_notify表中查询该条数据是否存在为审核的代办和通知,如果有,则需要进行逻辑删除,或者进行不展示处理
+        List<WorkProjectNotify> listByNotifyId = workProjectNotifyService.getListByNotifyId(entity.getId());// 查询该条数据是否存在为审核的代办和通知
+        if (listByNotifyId != null && !listByNotifyId.isEmpty()) {
+            //循环 对这些数据进行逻辑删除
+            for (WorkProjectNotify notify : listByNotifyId) {
+                notify.setDelFlag(WorkProjectNotify.DEL_FLAG_DELETE);
+                workProjectNotifyService.modifyDelflag(notify);
+            }
+        }
+
     }
 
 
@@ -787,7 +1101,7 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
      */
     public boolean canWithdraw(WorkKnowledgeBaseShareInfo entity) {
         if (entity == null || entity.getAuditStatus() == null) return false;
-        if (entity.getAuditStatus() != AUDIT_STATUS_PENDING && entity.getAuditStatus() != AUDIT_STATUS_AUDITING) return false;
+        if (!AUDIT_STATUS_PENDING.equals(entity.getAuditStatus()) && !AUDIT_STATUS_AUDITING.equals(entity.getAuditStatus())) return false;
         User currentUser = UserUtils.getUser();
         return currentUser.isAdmin() || UserUtils.isManager()
                 || currentUser.getId().equals(entity.getSubmitAuditUserId());
@@ -795,13 +1109,16 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
 
     /**
      * 判断当前用户是否可以审核(通过/驳回)该文件
+     * 包含主审专家抢办逻辑:如果指定了主审专家,非主审专家只有在剩余名额内才能审核
      */
     public boolean canAudit(WorkKnowledgeBaseShareInfo entity) {
         if (entity == null || entity.getAuditStatus() == null) return false;
-        if (entity.getAuditStatus() != AUDIT_STATUS_PENDING && entity.getAuditStatus() != AUDIT_STATUS_AUDITING) return false;
+        if (!AUDIT_STATUS_PENDING.equals(entity.getAuditStatus()) && !AUDIT_STATUS_AUDITING.equals(entity.getAuditStatus())) return false;
         String currentUserId = UserUtils.getUser().getId();
         // 提交审核人不可审核自己的文件
         if (currentUserId.equals(entity.getSubmitAuditUserId())) return false;
+        // 创建人不可审核自己的文件
+        if (entity.getCreateBy() != null && currentUserId.equals(entity.getCreateBy().getId())) return false;
         
         // 检查是否为典型案例(原创),只有专家可以审核
         boolean isTypicalCaseOriginal = false;
@@ -820,8 +1137,56 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
             }
         }
         
-        // 本轮未审核过的用户可审核(按 audit_start_date 隔离历史记录)
-        return auditRecordDao.countByFileIdAndUserIdSinceTime(entity.getId(), currentUserId, entity.getAuditStartDate()) == 0;
+        // 本轮未审核过的用户才可审核(按 audit_start_date 隔离历史记录)
+        Date startTime = entity.getAuditStartDate();
+        if (auditRecordDao.countByFileIdAndUserIdSinceTime(entity.getId(), currentUserId, startTime) > 0) {
+            return false;
+        }
+        
+        // ========== 主审专家抢办逻辑(仅对需要专家审核的分类生效) ==========
+        boolean needsExpertAudit = false;
+        if (StringUtils.isNotBlank(entity.getTreeNodeId())) {
+            WorkKnowledgeBaseTreeInfo treeInfo = getTreeInfoById(entity.getTreeNodeId());
+            if (treeInfo != null) {
+                String treeName = treeInfo.getTreeName();
+                needsExpertAudit = "技术总结".equals(treeName) || "培训心得".equals(treeName)
+                        || ("典型案例".equals(treeName) && "0".equals(entity.getContentAttribute()));
+            }
+        }
+        if (needsExpertAudit) {
+            String expertReviewerIds = entity.getExpertReviewerIds();
+            if (StringUtils.isNotBlank(expertReviewerIds)) {
+                Set<String> mainExpertIdSet = new HashSet<>(Arrays.asList(expertReviewerIds.split(",")));
+                boolean isMainExpert = mainExpertIdSet.contains(currentUserId);
+                if (isMainExpert) {
+                    return true; // 主审专家始终可以审核
+                }
+                // 非主审专家:检查剩余名额
+                int mainExpertCount = 0;
+                for (String eid : mainExpertIdSet) {
+                    if (StringUtils.isNotBlank(eid.trim())) {
+                        mainExpertCount++;
+                    }
+                }
+                int nonMainSlots = AUDIT_PASS_REQUIRED - mainExpertCount;
+                if (nonMainSlots <= 0) {
+                    return false;
+                }
+                // 统计本轮非主审专家已审核人数
+                int nonMainAuditedCount = 0;
+                List<WorkKnowledgeBaseAuditRecord> currentRecords = auditRecordDao.findByFileIdSinceTime(entity.getId(), startTime);
+                if (currentRecords != null) {
+                    for (WorkKnowledgeBaseAuditRecord rec : currentRecords) {
+                        if (!mainExpertIdSet.contains(rec.getAuditUserId())) {
+                            nonMainAuditedCount++;
+                        }
+                    }
+                }
+                return nonMainAuditedCount < nonMainSlots;
+            }
+        }
+        
+        return true;
     }
 
     /**
@@ -829,8 +1194,106 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
      */
     public boolean canSubmitAudit(WorkKnowledgeBaseShareInfo entity) {
         if (entity == null || entity.getAuditStatus() == null) return false;
-        return entity.getAuditStatus() == AUDIT_STATUS_PENDING
-                || entity.getAuditStatus() == AUDIT_STATUS_REJECTED;
+        return AUDIT_STATUS_PENDING.equals(entity.getAuditStatus())
+                || AUDIT_STATUS_REJECTED.equals(entity.getAuditStatus());
+    }
+
+    /**
+     * 列表页专用:根据行数据判断当前用户是否可以审核
+     * 包含主审专家抢办名额精确计算(查询本轮审核记录)
+     *
+     * @param row 列表行数据(来自 findDynamicList SQL 结果)
+     * @param currentUserId 当前登录用户ID
+     * @param isAdmin 当前用户是否为管理员
+     * @param isExpert 当前用户是否为专家
+     * @return 是否可以审核
+     */
+    public boolean checkCanAuditForList(Map<String, Object> row, String currentUserId, boolean isAdmin, boolean isExpert) {
+        Object auditStatusObj = row.get("auditStatus");
+        if (auditStatusObj == null) return false;
+        String auditStatus = auditStatusObj.toString();
+        // 只有未审核(1)和审核中(2)状态才显示审核按钮
+        if (!"1".equals(auditStatus) && !"2".equals(auditStatus)) return false;
+
+        // 创建人不能审核自己的文件
+        Object submitAuditUserIdObj = row.get("submitAuditUserId");
+        Object createByIdObj = row.get("createById");
+        String submitAuditUserId = submitAuditUserIdObj != null ? submitAuditUserIdObj.toString() : "";
+        String createById = createByIdObj != null ? createByIdObj.toString() : "";
+        boolean isCreator = StringUtils.isNotBlank(submitAuditUserId)
+                ? currentUserId.equals(submitAuditUserId)
+                : currentUserId.equals(createById);
+        if (isCreator) return false;
+
+        // 当前用户已审核过则不再显示审核按钮
+        Object currentUserAuditedObj = row.get("currentUserAudited");
+        if (currentUserAuditedObj != null && Boolean.TRUE.equals(currentUserAuditedObj)) {
+            return false;
+        }
+
+        // 判断是否为需要专家审核的分类
+        Object treeNameObj = row.get("treeName");
+        Object contentAttrObj = row.get("contentAttribute");
+        String treeName = treeNameObj != null ? treeNameObj.toString() : "";
+        String contentAttribute = contentAttrObj != null ? contentAttrObj.toString() : "";
+        boolean needsExpertAudit = "技术总结".equals(treeName) || "培训心得".equals(treeName)
+                || ("典型案例".equals(treeName) && "0".equals(contentAttribute));
+
+        // 基本权限:管理员、专家或普通分类
+        if (!isAdmin && !isExpert && needsExpertAudit) return false;
+
+        // ========== 主审专家抢办名额精确计算(仅对需要专家审核的分类生效) ==========
+        if (needsExpertAudit) {
+            Object expertIdsObj = row.get("expertReviewerIds");
+            String expertReviewerIds = expertIdsObj != null ? expertIdsObj.toString() : "";
+            if (StringUtils.isNotBlank(expertReviewerIds)) {
+                Set<String> mainExpertIdSet = new HashSet<>();
+                for (String eid : expertReviewerIds.split(",")) {
+                    if (StringUtils.isNotBlank(eid.trim())) {
+                        mainExpertIdSet.add(eid.trim());
+                    }
+                }
+                if (!mainExpertIdSet.isEmpty()) {
+                    boolean isMainExpert = mainExpertIdSet.contains(currentUserId);
+                    if (isAdmin || isMainExpert) {
+                        return true; // 管理员和主审专家始终可以审核
+                    }
+                    // 非主审专家:计算剩余名额
+                    int mainExpertCount = mainExpertIdSet.size();
+                    int nonMainSlots = AUDIT_PASS_REQUIRED - mainExpertCount;
+                    if (nonMainSlots <= 0) {
+                        return false;
+                    }
+                    // 查询本轮审核记录,统计非主审专家已审核人数
+                    Object fileIdObj = row.get("id");
+                    if (fileIdObj != null) {
+                        Object auditStartDateObj = row.get("auditStartDate");
+                        Date startTime = null;
+                        if (auditStartDateObj != null) {
+                            if (auditStartDateObj instanceof Date) {
+                                startTime = (Date) auditStartDateObj;
+                            }
+                        }
+                        if (startTime != null) {
+                            List<WorkKnowledgeBaseAuditRecord> records = auditRecordDao.findByFileIdSinceTime(fileIdObj.toString(), startTime);
+                            int nonMainAuditedCount = 0;
+                            if (records != null) {
+                                for (WorkKnowledgeBaseAuditRecord rec : records) {
+                                    if (!mainExpertIdSet.contains(rec.getAuditUserId())) {
+                                        nonMainAuditedCount++;
+                                    }
+                                }
+                            }
+                            return nonMainAuditedCount < nonMainSlots;
+                        }
+                    }
+                    // 无法确定时默认放行(详情页会精确校验)
+                    return true;
+                }
+            }
+        }
+
+        return true;
     }
 
     /**
@@ -1122,6 +1585,8 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         detail.setCategoryId(categoryId);
         detail.setRuleId(ruleId);
         detail.setPoint(point);
+        // 非手动赋分,清空手动赋分相关字段
+        detail.setIsManualScore("0");
         detail.preInsert();
         pointDetailDao.insert(detail);
 
@@ -1130,6 +1595,40 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     }
 
     /**
+     * 插入积分明细(手动赋分专用)
+     * @param userId 用户ID
+     * @param shareId 文件ID
+     * @param changeType 变动类型(3:手动赋分加分)
+     * @param categoryId 分类ID
+     * @param ruleId 规则ID
+     * @param point 积分值
+     * @param scoreUserId 赋分人ID
+     * @param targetUserId 被赋分人ID
+     * @param manualScoreTime 赋分时间
+     */
+    private void insertPointDetailForManualScore(String userId, String shareId, Integer changeType, String categoryId, String ruleId, Integer point,
+                                                   String scoreUserId, String targetUserId, java.util.Date manualScoreTime) {
+        WorkKnowledgeBasePointDetail detail = new WorkKnowledgeBasePointDetail();
+        detail.setId(IdGen.uuid());
+        detail.setUserId(userId);
+        detail.setShareId(shareId);
+        detail.setChangeType(changeType);
+        detail.setCategoryId(categoryId);
+        detail.setRuleId(ruleId);
+        detail.setPoint(point);
+        // 手动赋分标记
+        detail.setIsManualScore("1");
+        detail.setManualScoreUserId(scoreUserId);
+        detail.setManualScoreTargetUserId(targetUserId);
+        detail.setManualScoreTime(manualScoreTime);
+        detail.preInsert();
+        pointDetailDao.insert(detail);
+
+        // 确保该用户的兑换标记记录存在
+        ensureExchangeFlagExists(userId);
+    }
+
+    /**
      * 确保用户的兑换标记记录存在,不存在则自动创建
      * @param userId 用户ID
      */
@@ -1727,4 +2226,179 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     public void incrementClickCount(String id) {
         dao.incrementClickCount(id);
     }
+
+    // ==================== 手动赋分功能(独立于审核流程) ====================
+
+    /**
+     * 判断文件是否可以进行手动赋分
+     * 条件:未审核通过 且 非问答答疑分类
+     */
+    public boolean canManualScore(WorkKnowledgeBaseShareInfo entity) {
+        if (entity == null || entity.getAuditStatus() == null) return false;
+        String status = entity.getAuditStatus();
+        // 已审核通过的不需要
+        if (AUDIT_STATUS_PASSED.equals(status)) return false;
+        // 问答答疑分类不走手动赋分
+        if (QA_STATUS_WAITING.equals(status) || QA_STATUS_ANSWERING.equals(status) || QA_STATUS_CLOSED.equals(status)) return false;
+        return true;
+    }
+
+    /**
+     * 根据文档所属类别获取对应的赋分类型选项列表
+     * 规则:
+     *   1. 只从"贡献积分"下获取规则(排除"基础积分")
+     *   2. 特定分类(技术总结、培训心得、问答答疑、典型案例原创/转载)直接匹配其下的规则
+     *   3. 其余所有分类都走"相关资料"分类下的规则
+     * @param treeNodeId 文档所属树节点ID
+     * @param contentAttribute 内容属性(0=原创 1=转载)
+     * @return 赋分类型选项列表(每项包含 id, name,id为积分规则ID,name为规则名称pointName)
+     */
+    public List<Map<String, Object>> getScoreTypeOptions(String treeNodeId, String contentAttribute) {
+        List<Map<String, Object>> options = new ArrayList<>();
+        if (StringUtils.isBlank(treeNodeId)) {
+            return options;
+        }
+        WorkKnowledgeBaseTreeInfo treeInfo = getTreeInfoById(treeNodeId);
+        if (treeInfo == null) {
+            return options;
+        }
+        String treeName = treeInfo.getTreeName();
+
+        // 1. 查找"贡献积分"一级分类
+        List<WorkKnowledgePointCategory> contributeCats = findCategoriesByName("贡献积分");
+        if (contributeCats == null || contributeCats.isEmpty()) {
+            // 如果没有配置贡献积分分类,返回空列表
+            return options;
+        }
+        String contributeCategoryId = contributeCats.get(0).getId();
+
+        // 2. 查询"贡献积分"下的所有子分类
+        List<WorkKnowledgePointCategory> children = categoryDao.findChildrenByParentId(contributeCategoryId);
+        if (children == null || children.isEmpty()) {
+            return options;
+        }
+
+        // 3. 确定目标分类名称
+        String targetCategoryName = treeName;
+        if ("典型案例".equals(treeName)) {
+            // 典型案例:根据contentAttribute区分原创/转载
+            if ("0".equals(contentAttribute)) {
+                targetCategoryName = "典型案例(原创)";
+            } else {
+                targetCategoryName = "典型案例(转载)";
+            }
+        }
+
+        // 4. 定义特定分类列表(这些分类直接匹配)
+        Set<String> specialCategories = new HashSet<>();
+        specialCategories.add("技术总结");
+        specialCategories.add("培训心得");
+        specialCategories.add("问答答疑");
+        specialCategories.add("典型案例(原创)");
+        specialCategories.add("典型案例(转载)");
+
+        // 5. 判断是否为特定分类
+        boolean isSpecialCategory = specialCategories.contains(targetCategoryName);
+        
+        String finalCategoryId = null;
+        if (isSpecialCategory) {
+            // 特定分类:在贡献积分的子分类中查找匹配的
+            for (WorkKnowledgePointCategory child : children) {
+                if (targetCategoryName.equals(child.getCategoryName())) {
+                    finalCategoryId = child.getId();
+                    break;
+                }
+            }
+        } else {
+            // 非特定分类:使用"相关资料"分类
+            for (WorkKnowledgePointCategory child : children) {
+                if ("相关资料".equals(child.getCategoryName())) {
+                    finalCategoryId = child.getId();
+                    break;
+                }
+            }
+        }
+
+        // 6. 如果找到目标分类,查询其下的启用积分规则
+        if (StringUtils.isNotBlank(finalCategoryId)) {
+            addRulesToOptions(options, finalCategoryId);
+        }
+
+        return options;
+    }
+
+    /**
+     * 将指定分类下的启用积分规则添加到选项列表
+     * @param options 选项列表
+     * @param categoryId 分类ID
+     */
+    private void addRulesToOptions(List<Map<String, Object>> options, String categoryId) {
+        List<WorkKnowledgeBasePointRule> rules = pointRuleDao.findEnabledByCategoryId(categoryId);
+        if (rules != null && !rules.isEmpty()) {
+            for (WorkKnowledgeBasePointRule rule : rules) {
+                Map<String, Object> opt = new HashMap<>();
+                opt.put("id", rule.getId());           // 积分规则ID
+                opt.put("name", rule.getPointName());  // 规则名称(如"审核"、"创建")
+                opt.put("categoryId", categoryId);     // 所属分类ID(用于后续发放积分)
+                options.add(opt);
+            }
+        }
+    }
+
+    /**
+     * 手动赋分操作(独立于审核流程)
+     * 规则:
+     *   1. 仅未审核通过的数据可操作
+     *   2. 赋分后直接标记为审核通过
+     *   3. 给被赋分人发放积分(使用用户填写的分数)
+     *   4. 四个字段全部赋值
+     * @param fileId 文件ID
+     * @param scoreUserId 赋分人ID(当前操作人)
+     * @param targetUserId 被赋分人ID
+     * @param score 赋分分数(正整数)
+     * @param scoreRuleId 赋分类型对应的积分规则ID
+     */
+    @Transactional(readOnly = false)
+    public void manualScore(String fileId, String scoreUserId, String targetUserId, Integer score, String scoreRuleId) {
+        WorkKnowledgeBaseShareInfo entity = dao.get(fileId);
+        if (entity == null) {
+            throw new RuntimeException("文件记录不存在");
+        }
+        if (AUDIT_STATUS_PASSED.equals(entity.getAuditStatus())) {
+            throw new RuntimeException("该文件已审核通过,无需手动赋分");
+        }
+        if (score == null || score <= 0) {
+            throw new RuntimeException("赋分分数必须为正整数");
+        }
+        if (StringUtils.isBlank(targetUserId)) {
+            throw new RuntimeException("请选择被赋分人");
+        }
+        if (StringUtils.isBlank(scoreRuleId)) {
+            throw new RuntimeException("请选择赋分类型");
+        }
+
+        // 1. 查询选中的积分规则,获取其分类ID
+        WorkKnowledgeBasePointRule selectedRule = pointRuleDao.get(scoreRuleId);
+        if (selectedRule == null) {
+            throw new RuntimeException("积分规则不存在");
+        }
+        String categoryId = selectedRule.getCategoryId();
+        if (StringUtils.isBlank(categoryId)) {
+            throw new RuntimeException("积分规则未关联分类");
+        }
+
+        // 2. 更新文件状态为审核通过 + 写入手动赋分四个字段
+        entity.setManualScoreUserId(scoreUserId);
+        entity.setManualScoreTargetUserId(targetUserId);
+        entity.setManualScoreTime(new Date());
+        entity.setIsManualScore("1");
+        entity.setAuditStatus(AUDIT_STATUS_PASSED);
+        entity.preUpdate();
+        dao.updateManualScore(entity);
+
+        // 3. 给被赋分人发放积分(使用用户填写的分数值)
+        java.util.Date manualScoreTime = new Date();
+        insertPointDetailForManualScore(targetUserId, fileId, POINT_CHANGE_MANUAL, categoryId,
+                scoreRuleId, score, scoreUserId, targetUserId, manualScoreTime);
+    }
 }

+ 261 - 8
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBaseShareController.java

@@ -152,6 +152,14 @@ public class WorkKnowledgeBaseShareController extends BaseController {
                 boolean liked = readLikeService.isLiked(fileId, currentUserId);
                 map.put("liked", liked);
             }
+            
+            // 传递主审专家ID列表,用于前端判断审核权限
+            Object expertIdsObj = map.get("expertReviewerIds");
+            map.put("expertReviewerIds", expertIdsObj != null ? expertIdsObj.toString() : "");
+            
+            // 后端精确计算当前用户是否可以审核该文件(含主审专家抢办名额计算)
+            boolean rowCanAudit = shareService.checkCanAuditForList(map, currentUserId, isAdmin, isExpert);
+            map.put("canAudit", rowCanAudit);
         }
 
 
@@ -179,7 +187,7 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     /**
      * 详情页面(查看模式,不可编辑)
      */
-    @RequiresPermissions(value = {"workKnowledgeBase:share:view", "workKnowledgeBase:share:edit"}, logical = org.apache.shiro.authz.annotation.Logical.OR)
+    @RequiresPermissions(value = {"workKnowledgeBase:share:view", "workKnowledgeBase:share:edit", "workKnowledgeBase:share:audit"}, logical = org.apache.shiro.authz.annotation.Logical.OR)
     @RequestMapping(value = "detail")
     public String detail(@RequestParam(required = false) Boolean readOnly,
                         WorkKnowledgeBaseShareInfo shareInfo, Model model) {
@@ -228,14 +236,97 @@ public class WorkKnowledgeBaseShareController extends BaseController {
         }
 
         // 加载审核记录(审核中、审核通过、审核未通过状态时展示)
-        String auditStatus = shareInfo.getAuditStatus();
+        // 注意:必须从DB获取auditStatus,不能使用请求参数中的shareInfo(可能为null)
+        WorkKnowledgeBaseShareInfo workKnowledgeBaseShareInfo = shareService.get(shareInfo.getId());
+        String auditStatus = workKnowledgeBaseShareInfo.getAuditStatus();
+        List<WorkKnowledgeBaseAuditRecord> currentRoundAuditRecords = null;
         if (auditStatus != null && (auditStatus.equals("1") || auditStatus.equals("2") || auditStatus.equals("4") || auditStatus.equals("5"))) {
-            model.addAttribute("auditRecords", shareService.findAuditRecordsByFileId(shareInfo.getId()));
+            currentRoundAuditRecords = shareService.findAuditRecordsByFileId(shareInfo.getId());
+            model.addAttribute("auditRecords", currentRoundAuditRecords);
         }
 
-        // 查询treeName,判断是否为需要评分的特殊分类或问答答疑分类
-        // 注意:必须使用文件本身的treeNodeId,而不是请求参数中的(可能是父节点或空)
-        WorkKnowledgeBaseShareInfo workKnowledgeBaseShareInfo = shareService.get(shareInfo.getId());
+        // ========== 判断当前用户是否可以审核(详情页精确计算) ==========
+        String currentUserId = UserUtils.getUser().getId();
+        boolean isAdmin = UserUtils.getUser().isAdmin();
+        boolean isExpert = !isAdmin && expertService.isUserBound(currentUserId);
+        boolean isCreator = (workKnowledgeBaseShareInfo.getCreateBy() != null 
+                && currentUserId.equals(workKnowledgeBaseShareInfo.getCreateBy().getId()));
+
+        // 查询treeName,判断是否为需要专家审核的分类
+        String treeNameForAudit = null;
+        if (StringUtils.isNotBlank(workKnowledgeBaseShareInfo.getTreeNodeId())) {
+            WorkKnowledgeBaseTreeInfo treeInfo = shareService.getTreeInfoById(workKnowledgeBaseShareInfo.getTreeNodeId());
+            if (treeInfo != null) {
+                treeNameForAudit = treeInfo.getTreeName();
+            }
+        }
+        boolean needsExpertAudit = "技术总结".equals(treeNameForAudit) || "培训心得".equals(treeNameForAudit)
+                || ("典型案例".equals(treeNameForAudit) && "0".equals(workKnowledgeBaseShareInfo.getContentAttribute()));
+
+        // 检查当前用户本轮是否已审核过
+        boolean currentUserAudited = false;
+        if (currentRoundAuditRecords != null) {
+            for (WorkKnowledgeBaseAuditRecord rec : currentRoundAuditRecords) {
+                if (currentUserId.equals(rec.getAuditUserId())) {
+                    currentUserAudited = true;
+                    break;
+                }
+            }
+        }
+
+        // 解析主审专家ID集合
+        String expertReviewerIds = workKnowledgeBaseShareInfo.getExpertReviewerIds();
+        Set<String> mainExpertIdSet = new HashSet<>();
+        boolean isMainExpert = false;
+        List<String> mainExpertNames = new ArrayList<>();
+        if (StringUtils.isNotBlank(expertReviewerIds)) {
+            for (String eid : expertReviewerIds.split(",")) {
+                if (StringUtils.isNotBlank(eid)) {
+                    mainExpertIdSet.add(eid.trim());
+                    if (currentUserId.equals(eid.trim())) {
+                        isMainExpert = true;
+                    }
+                    // 解析用户名为展示用
+                    com.jeeplus.modules.sys.entity.User expertUser = UserUtils.get(eid.trim());
+                    if (expertUser != null && StringUtils.isNotBlank(expertUser.getName())) {
+                        mainExpertNames.add(expertUser.getName());
+                    }
+                }
+            }
+        }
+        model.addAttribute("mainExpertNames", mainExpertNames);
+
+        // 判断当前用户是否有权审核
+        boolean canAudit = false;
+        if (("1".equals(auditStatus) || "2".equals(auditStatus)) && !isCreator && !currentUserAudited) {
+            // 基本权限检查:管理员、专家或普通分类
+            if (isAdmin || isExpert || !needsExpertAudit) {
+                if (!needsExpertAudit || mainExpertIdSet.isEmpty()) {
+                    // 普通文件或没有指定主审专家,满足基本权限即可审核
+                    canAudit = true;
+                } else if (isMainExpert) {
+                    // 是指定主审专家,始终可以审核
+                    canAudit = true;
+                } else {
+                    // 非主审专家:只有还有剩余名额才可以审核(抢办)
+                    int mainExpertCount = mainExpertIdSet.size();
+                    int nonMainSlots = WorkKnowledgeBaseShareService.AUDIT_PASS_REQUIRED - mainExpertCount;
+                    // 计算本轮非主审专家已审核人数
+                    int nonMainAuditedCount = 0;
+                    if (currentRoundAuditRecords != null) {
+                        for (WorkKnowledgeBaseAuditRecord rec : currentRoundAuditRecords) {
+                            String recUserId = rec.getAuditUserId();
+                            if (!mainExpertIdSet.contains(recUserId)) {
+                                nonMainAuditedCount++;
+                            }
+                        }
+                    }
+                    canAudit = nonMainAuditedCount < nonMainSlots;
+                }
+            }
+        }
+        model.addAttribute("canAudit", canAudit);
+
         if (StringUtils.isNotBlank(workKnowledgeBaseShareInfo.getTreeNodeId())) {
             WorkKnowledgeBaseTreeInfo treeInfo = shareService.getTreeInfoById(workKnowledgeBaseShareInfo.getTreeNodeId());
             if (treeInfo != null) {
@@ -319,10 +410,14 @@ public class WorkKnowledgeBaseShareController extends BaseController {
 
         // 判断是否为问答答疑分类
         boolean isQaCategory = false;
+        String treeName = null;
         if (StringUtils.isNotBlank(treeNodeId)) {
             WorkKnowledgeBaseTreeInfo treeInfo = shareService.getTreeInfoById(treeNodeId);
-            if (treeInfo != null && "问答答疑".equals(treeInfo.getTreeName())) {
-                isQaCategory = true;
+            if (treeInfo != null) {
+                treeName = treeInfo.getTreeName();
+                if ("问答答疑".equals(treeName)) {
+                    isQaCategory = true;
+                }
             }
         }
 
@@ -331,6 +426,7 @@ public class WorkKnowledgeBaseShareController extends BaseController {
         model.addAttribute("rootId", rootId);
         model.addAttribute("dynValues", dynValues);
         model.addAttribute("isQaCategory", isQaCategory);
+        model.addAttribute("treeName", treeName);
         return "modules/WorkKnowledgeBase/workKnowledgeBaseShareForm";
     }
 
@@ -649,6 +745,7 @@ public class WorkKnowledgeBaseShareController extends BaseController {
                 exportHeaders.add("创建人|createByName");
                 exportHeaders.add("创建时间|createTime");
                 exportHeaders.add("状态|statusLabel");
+                exportHeaders.add("是否人工赋分|isManualScoreLabel");
                 exportHeaders.add("所属子节点|childNodeName");
 
                 // 查询该根节点下所有数据(通过rootId过滤)
@@ -659,6 +756,11 @@ public class WorkKnowledgeBaseShareController extends BaseController {
                 }
                 queryEntity.setBeginDate(beginDate);
                 queryEntity.setEndDate(endDate);
+                // 手动赋分筛选条件
+                String isManualScore = request.getParameter("isManualScore");
+                if (StringUtils.isNotBlank(isManualScore)) {
+                    queryEntity.setIsManualScore(isManualScore);
+                }
                 List<Map<String, Object>> dataList = shareService.findDynamicListAll(queryEntity, rootDynamicFields, dynamicConditions);
 
                 // 处理导出数据
@@ -682,6 +784,9 @@ public class WorkKnowledgeBaseShareController extends BaseController {
                         default: statusLabel = ""; break;
                     }
                     exportRow.put("statusLabel", statusLabel);
+                    // 是否人工赋分转换
+                    String manualScore = row.get("isManualScore") != null ? row.get("isManualScore").toString() : "";
+                    exportRow.put("isManualScoreLabel", "1".equals(manualScore) ? "是" : "否");
                     // 内容属性转换
                     String contentAttr = row.get("contentAttribute") != null ? row.get("contentAttribute").toString() : "";
                     if ("0".equals(contentAttr)) {
@@ -774,6 +879,50 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     }
 
     /**
+     * 审核通过(表单提交方式,返回重定向)
+     * 用于从sysHome.jsp的openDialogre打开的审核详情页面
+     */
+    @RequiresPermissions("workKnowledgeBase:share:audit")
+    @RequestMapping(value = "auditPassForm")
+    public String auditPassForm(@RequestParam String id,
+                                @RequestParam(required = false) String auditOpinion,
+                                @RequestParam(required = false) Integer score,
+                                @RequestParam(required = false) String home,
+                                @RequestParam(required = false) Integer result) {
+        try {
+            String currentUserId = UserUtils.getUser().getId();
+            
+            // 判断是通过还是驳回(result=1或null为通过,result=2为驳回)
+            if (result != null && result == 2) {
+                // 驳回
+                shareService.auditReject(id, currentUserId, auditOpinion);
+            } else {
+                // 通过
+                shareService.auditPass(id, currentUserId, auditOpinion, score);
+            }
+            
+            // 根据home参数决定重定向到哪个页面
+            if (StringUtils.isNotBlank(home) && "knowledgeList".equals(home)) {
+                // 从知识库列表页打开的,重定向回知识库列表页
+                return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/share/list?repage";
+            } else if (StringUtils.isNotBlank(home) && "notifyList".equals(home)) {
+                // 从通知列表打开的,重定向回通知列表页
+                return "redirect:" + Global.getAdminPath() + "/workprojectnotify/workProjectNotify/list?repage";
+            } else {
+                // 默认重定向到首页
+                return "redirect:" + Global.getAdminPath() + "/home/?repage";
+            }
+        } catch (Exception e) {
+            // 发生错误时重定向到列表页并携带错误消息
+            try {
+                return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/share/list?message=" + java.net.URLEncoder.encode(e.getMessage(), "UTF-8");
+            } catch (Exception ex) {
+                return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/share/list";
+            }
+        }
+    }
+
+    /**
      * 审核驳回
      */
     @RequiresPermissions("workKnowledgeBase:share:audit")
@@ -1146,4 +1295,108 @@ public class WorkKnowledgeBaseShareController extends BaseController {
         }
         return false;
     }
+
+    // ==================== 手动赋分接口(独立于审核流程) ====================
+
+    /**
+     * 手动赋分页面
+     */
+    @RequiresPermissions("workKnowledgeBase:share:audit")
+    @RequestMapping(value = "manualScoreForm")
+    public String manualScoreForm(@RequestParam String id, Model model) {
+        WorkKnowledgeBaseShareInfo entity = shareService.get(id);
+        if (entity == null) {
+            addMessage(model, "记录不存在!");
+            return "error";
+        }
+        if (!shareService.canManualScore(entity)) {
+            addMessage(model, "该文件已审核通过,无法进行手动赋分");
+            return "error";
+        }
+
+        String treeNodeId = entity.getTreeNodeId();
+        WorkKnowledgeBaseTreeInfo treeInfo = shareService.getTreeInfoById(treeNodeId);
+        String treeName = treeInfo != null ? treeInfo.getTreeName() : "";
+
+        // 确定默认被赋分人:优先 submit_audit_user_id,否则 create_by
+        String defaultTargetUserId = entity.getSubmitAuditUserId();
+        if (StringUtils.isBlank(defaultTargetUserId) && entity.getCreateBy() != null) {
+            defaultTargetUserId = entity.getCreateBy().getId();
+        }
+        String defaultTargetUserName = "";
+        if (StringUtils.isNotBlank(defaultTargetUserId)) {
+            com.jeeplus.modules.sys.entity.User targetUser = UserUtils.get(defaultTargetUserId);
+            if (targetUser != null) {
+                defaultTargetUserName = targetUser.getName();
+            }
+        }
+
+        // 获取赋分类型选项(根据文档所属类别匹配 work_knowledge_point_category 表)
+        List<Map<String, Object>> scoreTypeOptions = shareService.getScoreTypeOptions(treeNodeId, entity.getContentAttribute());
+
+        // 当前赋分人(当前登录用户)
+        String currentUserId = UserUtils.getUser().getId();
+        String currentUserName = UserUtils.getUser().getName();
+
+        model.addAttribute("entity", entity);
+        model.addAttribute("treeName", treeName);
+        model.addAttribute("defaultTargetUserId", defaultTargetUserId);
+        model.addAttribute("defaultTargetUserName", defaultTargetUserName);
+        model.addAttribute("scoreTypeOptions", scoreTypeOptions);
+        model.addAttribute("currentUserId", currentUserId);
+        model.addAttribute("currentUserName", currentUserName);
+        return "modules/WorkKnowledgeBase/workKnowledgeBaseManualScore";
+    }
+
+    /**
+     * 手动赋分提交(Ajax)
+     */
+    @RequiresPermissions("workKnowledgeBase:share:audit")
+    @RequestMapping(value = "manualScoreSave")
+    @ResponseBody
+    public Map<String, Object> manualScoreSave(@RequestParam String id,
+                                                @RequestParam String targetUserId,
+                                                @RequestParam Integer score,
+                                                @RequestParam String scoreRuleId) {
+        Map<String, Object> result = new HashMap<>();
+        try {
+            String currentUserId = UserUtils.getUser().getId();
+            shareService.manualScore(id, currentUserId, targetUserId, score, scoreRuleId);
+            result.put("success", true);
+            result.put("message", "手动赋分成功,文档已审核通过");
+        } catch (Exception e) {
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 查询用户列表(Ajax,用于被赋分人选择)
+     */
+    @RequiresPermissions("workKnowledgeBase:share:audit")
+    @RequestMapping(value = "searchUsers")
+    @ResponseBody
+    public List<Map<String, String>> searchUsers(@RequestParam(required = false) String name) {
+        List<Map<String, String>> userList = new ArrayList<>();
+        try {
+            List<com.jeeplus.modules.sys.entity.User> users;
+            if (StringUtils.isNotBlank(name)) {
+                users = UserUtils.getUserByName(name);
+            } else {
+                users = UserUtils.getCompanyUserList();
+            }
+            if (users != null) {
+                for (com.jeeplus.modules.sys.entity.User u : users) {
+                    Map<String, String> item = new HashMap<>();
+                    item.put("id", u.getId());
+                    item.put("name", u.getName());
+                    userList.add(item);
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return userList;
+    }
 }

+ 107 - 0
src/main/java/com/jeeplus/modules/sys/web/OfficeController.java

@@ -33,6 +33,8 @@ import com.jeeplus.modules.workclientinfo.entity.WorkClientLinkman;
 import com.jeeplus.modules.workstaff.entity.WorkStaffBasicInfo;
 import com.jeeplus.modules.workstaff.entity.WorkStaffCertificate;
 import com.jeeplus.modules.workstaff.service.WorkStaffBasicInfoService;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeExpert;
+import com.jeeplus.modules.WorkKnowledgeBase.service.WorkKnowledgeExpertService;
 import org.apache.commons.beanutils.PropertyUtils;
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.shiro.authz.annotation.Logical;
@@ -80,6 +82,8 @@ public class OfficeController extends BaseController {
     private WorkStaffBasicInfoService workStaffBasicInfoService;
     @Autowired
     private OfficeDao officeDao;
+    @Autowired
+    private WorkKnowledgeExpertService expertService;
 
     @ModelAttribute("office")
     public Office get(@RequestParam(required = false) String id) {
@@ -632,6 +636,109 @@ public class OfficeController extends BaseController {
         return mapList;
 
     }
+
+    /**
+     * 获取专家树形数据(专门用于知识库主审专家选择)
+     * 返回启用的专家所属的部门节点,按部门分组展示专家用户
+     *
+     * @param extId       排除的ID
+     * @param selectName  搜索关键词(专家姓名模糊查询)
+     * @param response
+     * @return
+     */
+    @RequiresPermissions("user")
+    @ResponseBody
+    @RequestMapping(value = "expertTreeData")
+    public List<Map<String, Object>> expertTreeData(@RequestParam(required = false) String extId, 
+                                                     @RequestParam(required = false) String selectName,
+                                                     HttpServletResponse response) {
+        List<Map<String, Object>> mapList = Lists.newArrayList();
+        
+        // 获取所有启用的专家
+        List<WorkKnowledgeExpert> expertList = expertService.findEnabledList();
+        if(expertList == null || expertList.isEmpty()){
+            return mapList;
+        }
+        
+        // 构建专家userId到专家的映射
+        Map<String, WorkKnowledgeExpert> expertMap = new HashMap<>();
+        for(WorkKnowledgeExpert expert : expertList){
+            if(StringUtils.isNotBlank(expert.getUserId())){
+                expertMap.put(expert.getUserId(), expert);
+            }
+        }
+        
+        // 收集所有专家所属的部门ID
+        Set<String> officeIds = new HashSet<>();
+        for(WorkKnowledgeExpert expert : expertList){
+            User user = UserUtils.get(expert.getUserId());
+            if(user != null && user.getOffice() != null && StringUtils.isNotBlank(user.getOffice().getId())){
+                officeIds.add(user.getOffice().getId());
+            }
+        }
+        
+        if(officeIds.isEmpty()){
+            return mapList;
+        }
+        
+        // 查询这些部门的信息
+        Office queryOffice = new Office();
+        queryOffice.setParentIds(UserUtils.getSelectCompany().getId());
+        List<Office> allOffices = officeService.findByParentIdsLike(queryOffice);
+        
+        // 过滤出包含专家的部门
+        for(Office office : allOffices){
+            if(StringUtils.isBlank(extId) || !extId.equals(office.getId())){
+                if(Global.YES.equals(office.getUseable()) && officeIds.contains(office.getId())){
+                    Map<String, Object> map = Maps.newHashMap();
+                    map.put("id", office.getId());
+                    map.put("pId", office.getParentId());
+                    map.put("pIds", office.getParentIds());
+                    map.put("name", office.getTopCompany());
+                    map.put("isParent", true); // 标记为父节点,下面有子节点(专家用户)
+                    mapList.add(map);
+                }
+            }
+        }
+        
+        // 为每个部门添加专家用户子节点
+        List<Map<String, Object>> userMaps = new ArrayList<>();
+        for(Map<String, Object> deptMap : mapList){
+            String officeId = deptMap.get("id").toString();
+            
+            // 查找属于该部门的专家
+            for(WorkKnowledgeExpert expert : expertList){
+                User user = UserUtils.get(expert.getUserId());
+                if(user != null && user.getOffice() != null && officeId.equals(user.getOffice().getId())){
+                    // 如果有搜索关键词,进行模糊匹配
+                    if(StringUtils.isNotBlank(selectName)){
+                        String userName = user.getName();
+                        String expertName = expert.getExpertName();
+                        // 匹配用户名或专家名包含搜索关键词
+                        if(!userName.contains(selectName) && !expertName.contains(selectName)){
+                            continue;
+                        }
+                    }
+                    
+                    Map<String, Object> userMap = Maps.newHashMap();
+                    userMap.put("id", "u_" + user.getId()); // 用户ID前加u_标识
+                    userMap.put("pId", officeId);
+                    userMap.put("name", user.getName());
+                    userMap.put("isParent", false); // 用户节点不是父节点
+                    userMap.put("expertId", expert.getId()); // 专家记录ID
+                    userMap.put("expertName", expert.getExpertName());
+                    userMap.put("professionalField", expert.getProfessionalField());
+                    userMap.put("phone", expert.getPhone());
+                    userMaps.add(userMap);
+                }
+            }
+        }
+        
+        // 将所有用户节点添加到结果列表
+        mapList.addAll(userMaps);
+        
+        return mapList;
+    }
     /**
      * 获取机构JSON数据。
      *

+ 11 - 0
src/main/java/com/jeeplus/modules/workprojectnotify/dao/WorkProjectNotifyDao.java

@@ -59,6 +59,9 @@ public interface WorkProjectNotifyDao extends CrudDao<WorkProjectNotify> {
     //逻辑删除
     int updateDelflagByNotifyId(WorkProjectNotify workProjectNotify);
 
+    //按通知ID和用户逻辑删除
+    int updateDelflagByNotifyIdAndUserId(WorkProjectNotify workProjectNotify);
+
     int updateReadStateByNotifyIdAndNotifyRole(WorkProjectNotify notify);
 
     int updateReadStateByNotifyIdAndNotifyUser(WorkProjectNotify notify);
@@ -151,4 +154,12 @@ public interface WorkProjectNotifyDao extends CrudDao<WorkProjectNotify> {
      * @return
      */
     List<WorkProjectNotify> getByNotifyIdAndNotifyUserAndTitle(@Param("title") String title, @Param("notifyId")String notifyId, @Param("notifyUserId")String notifyUserId);
+
+
+    /**
+     * 根据notifyId查询所有的数据
+     * @param notifyId
+     * @return
+     */
+    List<WorkProjectNotify> getListByNotifyId(@Param("notifyId") String notifyId);
 }

+ 17 - 0
src/main/java/com/jeeplus/modules/workprojectnotify/service/WorkProjectNotifyService.java

@@ -52,6 +52,9 @@ public class WorkProjectNotifyService extends CrudService<WorkProjectNotifyDao,
 	public WorkProjectNotify getByNotifyId(String id) {
 		return dao.getByNotifyId(id);
 	}
+	public List<WorkProjectNotify> getListByNotifyId(String id) {
+		return dao.getListByNotifyId(id);
+	}
 
 	public WorkProjectNotify getByInfo(WorkProjectNotify workProjectNotify) {
 		return dao.getByInfo(workProjectNotify);
@@ -370,6 +373,20 @@ public class WorkProjectNotifyService extends CrudService<WorkProjectNotifyDao,
 	}
 
 	@Transactional(readOnly = false)
+	public int modifyDelflagByUser(WorkProjectNotify workProjectNotify){
+		workProjectNotify.setDelFlag("1");
+		workProjectNotify.preUpdate();
+		return dao.updateDelflagByNotifyIdAndUserId(workProjectNotify);
+	}
+
+	@Transactional(readOnly = false)
+	public boolean updateNewReadStateByNotifyId(WorkProjectNotify workProjectNotify){
+		workProjectNotify.setDelFlag("1");
+		workProjectNotify.preUpdate();
+		return dao.updateNewReadStateByNotifyId(workProjectNotify);
+	}
+
+	@Transactional(readOnly = false)
 	public int readByNotifyIdAndNotifyRole(WorkProjectNotify notify) {
 		notify.setStatus("1");
 		notify.preUpdate();

+ 198 - 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.WorkKnowledgeExpertService;
 import com.jeeplus.modules.act.entity.Act;
 import com.jeeplus.modules.act.service.ActTaskService;
 import com.jeeplus.modules.businessQuestions.entity.BusinessQuestions;
@@ -637,6 +638,9 @@ public class WorkProjectNotifyController extends BaseController {
 	private WorkKnowledgeBaseShareService workKnowledgeBaseShareService;
 
 	@Autowired
+	private WorkKnowledgeExpertService expertService;
+
+	@Autowired
 	private WorkKnowledgeBasePointRuleService pointRuleService;
 
 	@Autowired
@@ -7791,6 +7795,8 @@ public class WorkProjectNotifyController extends BaseController {
 					return this.sendMilitaryIndustryConfidentiality(workProjectNotify, model);
 				} else if ("199".equals(workProjectNotify.getType())) {
 					return this.sendWorkKnowBaseShareInfo(workProjectNotify, model);
+				} else if ("200".equals(workProjectNotify.getType())) {
+					return this.specialistAuditWorkKnowBaseShareInfo(workProjectNotify, model);
 				} else if ("70".equals(workProjectNotify.getType())) {    //部门调转
 					WorkChangeJob workChangeJob = workChangeJobService.get(workProjectNotify.getNotifyId());
 					Act act = getByAct(workChangeJob.getProcessInstanceId());
@@ -11661,4 +11667,196 @@ public class WorkProjectNotifyController extends BaseController {
 		}
 	}
 
+	private String specialistAuditWorkKnowBaseShareInfo(WorkProjectNotify workProjectNotify, Model model) throws Exception {
+
+		WorkKnowledgeBaseShareInfo shareInfo = workKnowledgeBaseShareService.get(workProjectNotify.getNotifyId());
+		shareInfo.setHome("home");
+		if (shareInfo == null) {
+			addMessage(model, "记录不存在!");
+			return "error";
+		}
+
+		String rootId = shareInfo.getRootId();
+		String treeNodeId = shareInfo.getTreeNodeId();
+
+		if (StringUtils.isBlank(rootId) && StringUtils.isNotBlank(treeNodeId)) {
+			rootId = workKnowledgeBaseShareService.getRootIdByNodeId(treeNodeId);
+			shareInfo.setRootId(rootId);
+		}
+
+		// 查询动态字段配置
+		List<WorkKnowledgeBaseDynamicInfo> dynamicFields = new ArrayList<>();
+		if (StringUtils.isNotBlank(rootId)) {
+			dynamicFields = workKnowledgeBaseShareService.findDynamicFields(rootId);
+		}
+
+		// 加载动态字段已有值
+		Map<String, String> dynValues = new HashMap<>();
+		if (StringUtils.isNotBlank(shareInfo.getId())) {
+			List<WorkKnowledgeBaseDynamicValueInfo> existValues =
+					workKnowledgeBaseShareService.findDynamicValuesByFileId(shareInfo.getId());
+			if (existValues != null) {
+				for (WorkKnowledgeBaseDynamicValueInfo val : existValues) {
+					dynValues.put(val.getDynamicFieldId(), val.getFieldValue());
+				}
+			}
+		}
+
+		// 加载附件列表
+		if (StringUtils.isNotBlank(shareInfo.getId())) {
+			shareInfo.setWorkAttachments(workKnowledgeBaseShareService.findAttachmentsByFileId(shareInfo.getId()));
+		}
+
+		model.addAttribute("entity", shareInfo);
+		model.addAttribute("dynamicFields", dynamicFields);
+		model.addAttribute("rootId", rootId);
+		model.addAttribute("dynValues", dynValues);
+
+		boolean readOnly = false;
+		// 传递只读标识
+		if (workProjectNotify.getRemarks().contains("待通知")){
+			readOnly = true;
+		}
+		model.addAttribute("readOnly",  readOnly);
+
+		// 获取阅读量和点赞量
+		if (StringUtils.isNotBlank(shareInfo.getId())) {
+			model.addAttribute("readCount", readLikeService.getReadCount(shareInfo.getId()));
+			model.addAttribute("likeCount", readLikeService.getLikeCount(shareInfo.getId()));
+		}
+
+		// 加载审核记录(审核中、审核通过、审核未通过状态时展示)
+		// 注意:必须从DB获取auditStatus,不能使用请求参数中的shareInfo(可能为null)
+		WorkKnowledgeBaseShareInfo workKnowledgeBaseShareInfo = workKnowledgeBaseShareService.get(shareInfo.getId());
+		String auditStatus = workKnowledgeBaseShareInfo.getAuditStatus();
+		List<WorkKnowledgeBaseAuditRecord> currentRoundAuditRecords = null;
+		if (auditStatus != null && (auditStatus.equals("1") || auditStatus.equals("2") || auditStatus.equals("4") || auditStatus.equals("5"))) {
+			currentRoundAuditRecords = workKnowledgeBaseShareService.findAuditRecordsByFileId(shareInfo.getId());
+			model.addAttribute("auditRecords", currentRoundAuditRecords);
+		}
+
+		// ========== 判断当前用户是否可以审核(详情页精确计算) ==========
+		String currentUserId = UserUtils.getUser().getId();
+		boolean isAdmin = UserUtils.getUser().isAdmin();
+		boolean isExpert = !isAdmin && expertService.isUserBound(currentUserId);
+		boolean isCreator = (workKnowledgeBaseShareInfo.getCreateBy() != null
+				&& currentUserId.equals(workKnowledgeBaseShareInfo.getCreateBy().getId()));
+
+		// 查询treeName,判断是否为需要专家审核的分类
+		String treeNameForAudit = null;
+		if (StringUtils.isNotBlank(workKnowledgeBaseShareInfo.getTreeNodeId())) {
+			WorkKnowledgeBaseTreeInfo treeInfo = workKnowledgeBaseShareService.getTreeInfoById(workKnowledgeBaseShareInfo.getTreeNodeId());
+			if (treeInfo != null) {
+				treeNameForAudit = treeInfo.getTreeName();
+			}
+		}
+		boolean needsExpertAudit = "技术总结".equals(treeNameForAudit) || "培训心得".equals(treeNameForAudit)
+				|| ("典型案例".equals(treeNameForAudit) && "0".equals(workKnowledgeBaseShareInfo.getContentAttribute()));
+
+		// 检查当前用户本轮是否已审核过
+		boolean currentUserAudited = false;
+		if (currentRoundAuditRecords != null) {
+			for (WorkKnowledgeBaseAuditRecord rec : currentRoundAuditRecords) {
+				if (currentUserId.equals(rec.getAuditUserId())) {
+					currentUserAudited = true;
+					break;
+				}
+			}
+		}
+
+		// 解析主审专家ID集合
+		String expertReviewerIds = workKnowledgeBaseShareInfo.getExpertReviewerIds();
+		Set<String> mainExpertIdSet = new HashSet<>();
+		boolean isMainExpert = false;
+		List<String> mainExpertNames = new ArrayList<>();
+		if (StringUtils.isNotBlank(expertReviewerIds)) {
+			for (String eid : expertReviewerIds.split(",")) {
+				if (StringUtils.isNotBlank(eid)) {
+					mainExpertIdSet.add(eid.trim());
+					if (currentUserId.equals(eid.trim())) {
+						isMainExpert = true;
+					}
+					// 解析用户名为展示用
+					com.jeeplus.modules.sys.entity.User expertUser = UserUtils.get(eid.trim());
+					if (expertUser != null && StringUtils.isNotBlank(expertUser.getName())) {
+						mainExpertNames.add(expertUser.getName());
+					}
+				}
+			}
+		}
+		model.addAttribute("mainExpertNames", mainExpertNames);
+
+		// 判断当前用户是否有权审核
+		boolean canAudit = false;
+		if (("1".equals(auditStatus) || "2".equals(auditStatus)) && !isCreator && !currentUserAudited) {
+			// 基本权限检查:管理员、专家或普通分类
+			if (isAdmin || isExpert || !needsExpertAudit) {
+				if (!needsExpertAudit || mainExpertIdSet.isEmpty()) {
+					// 普通文件或没有指定主审专家,满足基本权限即可审核
+					canAudit = true;
+				} else if (isMainExpert) {
+					// 是指定主审专家,始终可以审核
+					canAudit = true;
+				} else {
+					// 非主审专家:只有还有剩余名额才可以审核(抢办)
+					int mainExpertCount = mainExpertIdSet.size();
+					int nonMainSlots = WorkKnowledgeBaseShareService.AUDIT_PASS_REQUIRED - mainExpertCount;
+					// 计算本轮非主审专家已审核人数
+					int nonMainAuditedCount = 0;
+					if (currentRoundAuditRecords != null) {
+						for (WorkKnowledgeBaseAuditRecord rec : currentRoundAuditRecords) {
+							String recUserId = rec.getAuditUserId();
+							if (!mainExpertIdSet.contains(recUserId)) {
+								nonMainAuditedCount++;
+							}
+						}
+					}
+					canAudit = nonMainAuditedCount < nonMainSlots;
+				}
+			}
+		}
+		model.addAttribute("canAudit", canAudit);
+
+		if (StringUtils.isNotBlank(workKnowledgeBaseShareInfo.getTreeNodeId())) {
+			WorkKnowledgeBaseTreeInfo treeInfo = workKnowledgeBaseShareService.getTreeInfoById(workKnowledgeBaseShareInfo.getTreeNodeId());
+			if (treeInfo != null) {
+				String treeName = treeInfo.getTreeName();
+				model.addAttribute("treeName", treeName);
+
+				// 判断是否为问答答疑分类
+				boolean isQaCategory = "问答答疑".equals(treeName);
+				model.addAttribute("isQaCategory", isQaCategory);
+
+				// 如果是技术总结或培训心得,查询对应的积分规则
+				if ("技术总结".equals(treeName) || "培训心得".equals(treeName)) {
+					// 根据treeName查询work_knowledge_point_category表获取categoryId
+					List<WorkKnowledgePointCategory> categories = workKnowledgeBaseShareService.findCategoriesByName(treeName);
+					if (categories != null && !categories.isEmpty()) {
+						String categoryId = categories.get(0).getId();
+						// 查询该分类下rule_type=2(创建积分)的规则
+						WorkKnowledgeBasePointRule rule = pointRuleService.findByCategoryIdAndRuleType(categoryId, "1");
+						if (rule != null) {
+							model.addAttribute("pointRule", rule);
+						}
+					}
+				}
+
+				// 如果是典型案例(原创),查询“典型案例(原创)”分类对应的积分规则
+				if ("典型案例".equals(treeName) && "0".equals(workKnowledgeBaseShareInfo.getContentAttribute())) {
+					List<WorkKnowledgePointCategory> categories = workKnowledgeBaseShareService.findCategoriesByName("典型案例(原创)");
+					if (categories != null && !categories.isEmpty()) {
+						String categoryId = categories.get(0).getId();
+						// 查询该分类下rule_type=1(创建积分)的规则
+						WorkKnowledgeBasePointRule rule = pointRuleService.findByCategoryIdAndRuleType(categoryId, "1");
+						if (rule != null) {
+							model.addAttribute("pointRule", rule);
+						}
+					}
+				}
+			}
+		}
+
+		return "modules/WorkKnowledgeBase/workKnowledgeBaseShareDetail";
+	}
+
 }

+ 59 - 30
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBasePointDetailDao.xml

@@ -10,6 +10,10 @@
         a.category_id   AS "categoryId",
         a.rule_id       AS "ruleId",
         a.point         AS "point",
+        a.is_manual_score AS "isManualScore",
+        a.manual_score_user_id AS "manualScoreUserId",
+        a.manual_score_target_user_id AS "manualScoreTargetUserId",
+        a.manual_score_time AS "manualScoreTime",
         a.create_by     AS "createBy.id",
         a.create_date   AS "createDate",
         a.update_by     AS "updateBy.id",
@@ -61,9 +65,11 @@
     <insert id="insert">
         INSERT INTO work_knowledge_base_point_detail (
             id, user_id, share_id, change_type, category_id, rule_id, point,
+            is_manual_score, manual_score_user_id, manual_score_target_user_id, manual_score_time,
             create_by, create_date, update_by, update_date, remarks, del_flag
         ) VALUES (
             #{id}, #{userId}, #{shareId}, #{changeType}, #{categoryId}, #{ruleId}, #{point},
+            #{isManualScore}, #{manualScoreUserId}, #{manualScoreTargetUserId}, #{manualScoreTime},
             #{createBy.id}, #{createDate}, #{updateBy.id}, #{updateDate}, #{remarks}, #{delFlag}
         )
     </insert>
@@ -442,67 +448,90 @@
             <if test="endDate != null and endDate != ''">
                 AND create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
             </if>
+            <if test="isManualScore != null and isManualScore != ''">
+                AND is_manual_score = #{isManualScore}
+            </if>
             GROUP BY COALESCE(submit_audit_user_id, create_by)
         ) uc ON uc.owner_id = u.id
         LEFT JOIN (
-            SELECT user_id, SUM(point) AS create_point
-            FROM work_knowledge_base_point_detail
-            WHERE del_flag = '0' AND change_type = 1 AND point > 0
+            SELECT pd.user_id, SUM(pd.point) AS create_point
+            FROM work_knowledge_base_point_detail pd
+            LEFT JOIN work_knowledge_base_share_info si ON si.id = pd.share_id AND si.del_flag = '0'
+            WHERE pd.del_flag = '0' AND pd.change_type = 1 AND pd.point > 0
             <if test="beginDate != null and beginDate != ''">
-                AND create_date &gt;= #{beginDate}
+                AND pd.create_date &gt;= #{beginDate}
             </if>
             <if test="endDate != null and endDate != ''">
-                AND create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+                AND pd.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
             </if>
-            GROUP BY user_id
+            <if test="isManualScore != null and isManualScore != ''">
+                AND si.is_manual_score = #{isManualScore}
+            </if>
+            GROUP BY pd.user_id
         ) cp ON cp.user_id = u.id
         LEFT JOIN (
-            SELECT user_id, COUNT(DISTINCT share_id) AS audit_count
-            FROM work_knowledge_base_point_detail
-            WHERE del_flag = '0' AND change_type = 2
+            SELECT pd.user_id, COUNT(DISTINCT pd.share_id) AS audit_count
+            FROM work_knowledge_base_point_detail pd
+            LEFT JOIN work_knowledge_base_share_info si ON si.id = pd.share_id AND si.del_flag = '0'
+            WHERE pd.del_flag = '0' AND pd.change_type = 2
             <if test="beginDate != null and beginDate != ''">
-                AND create_date &gt;= #{beginDate}
+                AND pd.create_date &gt;= #{beginDate}
             </if>
             <if test="endDate != null and endDate != ''">
-                AND create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+                AND pd.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
             </if>
-            GROUP BY user_id
+            <if test="isManualScore != null and isManualScore != ''">
+                AND si.is_manual_score = #{isManualScore}
+            </if>
+            GROUP BY pd.user_id
         ) ac ON ac.user_id = u.id
         LEFT JOIN (
-            SELECT user_id, SUM(point) AS audit_point
-            FROM work_knowledge_base_point_detail
-            WHERE del_flag = '0' AND change_type = 2 AND point > 0
+            SELECT pd.user_id, SUM(pd.point) AS audit_point
+            FROM work_knowledge_base_point_detail pd
+            LEFT JOIN work_knowledge_base_share_info si ON si.id = pd.share_id AND si.del_flag = '0'
+            WHERE pd.del_flag = '0' AND pd.change_type = 2 AND pd.point > 0
             <if test="beginDate != null and beginDate != ''">
-                AND create_date &gt;= #{beginDate}
+                AND pd.create_date &gt;= #{beginDate}
             </if>
             <if test="endDate != null and endDate != ''">
-                AND create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+                AND pd.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
             </if>
-            GROUP BY user_id
+            <if test="isManualScore != null and isManualScore != ''">
+                AND si.is_manual_score = #{isManualScore}
+            </if>
+            GROUP BY pd.user_id
         ) ap ON ap.user_id = u.id
         LEFT JOIN (
-            SELECT user_id, SUM(point) AS read_point
-            FROM work_knowledge_base_point_detail
-            WHERE del_flag = '0' AND change_type = 3 AND point > 0
+            SELECT pd.user_id, SUM(pd.point) AS read_point
+            FROM work_knowledge_base_point_detail pd
+            LEFT JOIN work_knowledge_base_share_info si ON si.id = pd.share_id AND si.del_flag = '0'
+            WHERE pd.del_flag = '0' AND pd.change_type = 3 AND pd.point > 0
             <if test="beginDate != null and beginDate != ''">
-                AND create_date &gt;= #{beginDate}
+                AND pd.create_date &gt;= #{beginDate}
             </if>
             <if test="endDate != null and endDate != ''">
-                AND create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+                AND pd.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
             </if>
-            GROUP BY user_id
+            <if test="isManualScore != null and isManualScore != ''">
+                AND si.is_manual_score = #{isManualScore}
+            </if>
+            GROUP BY pd.user_id
         ) rp ON rp.user_id = u.id
         LEFT JOIN (
-            SELECT user_id, SUM(point) AS like_point
-            FROM work_knowledge_base_point_detail
-            WHERE del_flag = '0' AND change_type = 4 AND point > 0
+            SELECT pd.user_id, SUM(pd.point) AS like_point
+            FROM work_knowledge_base_point_detail pd
+            LEFT JOIN work_knowledge_base_share_info si ON si.id = pd.share_id AND si.del_flag = '0'
+            WHERE pd.del_flag = '0' AND pd.change_type = 4 AND pd.point > 0
             <if test="beginDate != null and beginDate != ''">
-                AND create_date &gt;= #{beginDate}
+                AND pd.create_date &gt;= #{beginDate}
             </if>
             <if test="endDate != null and endDate != ''">
-                AND create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
+                AND pd.create_date &lt;= TIMESTAMP(CONCAT(#{endDate}, ' 23:59:59'))
             </if>
-            GROUP BY user_id
+            <if test="isManualScore != null and isManualScore != ''">
+                AND si.is_manual_score = #{isManualScore}
+            </if>
+            GROUP BY pd.user_id
         ) lp ON lp.user_id = u.id
         LEFT JOIN (
             SELECT user_id, SUM(ABS(point)) AS exchange_point

+ 63 - 4
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseShareInfoDao.xml

@@ -24,6 +24,11 @@
         a.confirm_date     AS "confirmDate",
         a.question_description AS "questionDescription",
         a.click_count        AS "clickCount",
+        a.is_manual_score    AS "isManualScore",
+        a.manual_score_user_id AS "manualScoreUserId",
+        a.manual_score_target_user_id AS "manualScoreTargetUserId",
+        a.manual_score_time  AS "manualScoreTime",
+        a.expert_reviewer_ids AS "expertReviewerIds",
         a.del_flag         AS "delFlag"
     </sql>
 
@@ -89,6 +94,8 @@
             a.confirm_date                                        AS "confirmDate",
             a.question_description                                AS "questionDescription",
             a.click_count                                          AS "clickCount",
+            a.is_manual_score                                      AS "isManualScore",
+            a.expert_reviewer_ids                                  AS "expertReviewerIds",
             su.name                                               AS "createBy.name"
             <!-- 动态列:CASE WHEN + MAX + GROUP BY -->
             <if test="dynamicFields != null and dynamicFields.size() > 0">
@@ -137,6 +144,10 @@
             <if test="entity.endDate != null and entity.endDate != ''">
                 AND a.create_date &lt;= CONCAT(#{entity.endDate}, ' 23:59:59')
             </if>
+            <!-- 是否手动赋分筛选 -->
+            <if test="entity.isManualScore != null and entity.isManualScore != ''">
+                AND a.is_manual_score = #{entity.isManualScore}
+            </if>
             <!-- 动态字段查询条件:EXISTS 子查询(禁止写在主WHERE中) -->
             <if test="dynamicConditions != null">
                 <foreach collection="dynamicConditions" index="fieldId" item="queryVal">
@@ -155,7 +166,7 @@
         </where>
         GROUP BY a.id, a.tree_node_id, a.file_name, a.file_url, a.name, a.create_time,
                  a.create_by, a.create_date, a.remarks, a.audit_status, a.audit_pass_count, a.audit_reject_count, a.submit_audit_user_id, a.audit_start_date,
-                 a.content_attribute, a.answer_id, a.confirm_date, a.question_description, a.click_count
+                 a.content_attribute, a.answer_id, a.confirm_date, a.question_description, a.click_count, a.is_manual_score, a.expert_reviewer_ids
         ORDER BY
             CASE a.audit_status
                 WHEN '2' THEN 1
@@ -204,6 +215,10 @@
             <if test="entity.endDate != null and entity.endDate != ''">
                 AND a.create_date &lt;= CONCAT(#{entity.endDate}, ' 23:59:59')
             </if>
+            <!-- 是否手动赋分筛选 -->
+            <if test="entity.isManualScore != null and entity.isManualScore != ''">
+                AND a.is_manual_score = #{entity.isManualScore}
+            </if>
             <!-- 动态字段查询条件:EXISTS 子查询 -->
             <if test="dynamicConditions != null">
                 <foreach collection="dynamicConditions" index="fieldId" item="queryVal">
@@ -255,12 +270,18 @@
             id, tree_node_id, file_name, file_url, name, create_time,content_attribute,
             create_by, create_date, update_by, update_date, remarks,
             audit_status, audit_pass_count, audit_reject_count, submit_audit_user_id, audit_start_date,
-            answer_id, confirm_date, question_description, del_flag
+            answer_id, confirm_date, question_description,
+            is_manual_score, manual_score_user_id, manual_score_target_user_id, manual_score_time,
+            expert_reviewer_ids,
+            del_flag
         ) VALUES (
             #{id}, #{treeNodeId}, #{fileName}, #{fileUrl}, #{name}, NOW(),#{contentAttribute},
             #{createBy.id}, #{createDate}, #{updateBy.id}, #{updateDate}, #{remarks},
             #{auditStatus}, #{auditPassCount}, #{auditRejectCount}, #{submitAuditUserId}, #{auditStartDate},
-            #{answerId}, #{confirmDate}, #{questionDescription}, #{delFlag}
+            #{answerId}, #{confirmDate}, #{questionDescription},
+            #{isManualScore}, #{manualScoreUserId}, #{manualScoreTargetUserId}, #{manualScoreTime},
+            #{expertReviewerIds},
+            #{delFlag}
         )
     </insert>
 
@@ -281,7 +302,12 @@
             audit_start_date   = #{auditStartDate},
             answer_id          = #{answerId},
             confirm_date       = #{confirmDate},
-            question_description = #{questionDescription}
+            question_description = #{questionDescription},
+            is_manual_score    = #{isManualScore},
+            manual_score_user_id = #{manualScoreUserId},
+            manual_score_target_user_id = #{manualScoreTargetUserId},
+            manual_score_time  = #{manualScoreTime},
+            expert_reviewer_ids = #{expertReviewerIds}
         WHERE id = #{id}
     </update>
 
@@ -327,6 +353,31 @@
         WHERE id = #{id}
     </update>
 
+    <!-- 手动赋分:更新审核状态 + 手动赋分相关字段 -->
+    <update id="updateManualScore">
+        UPDATE work_knowledge_base_share_info SET
+            audit_status       = '5',
+            is_manual_score    = '1',
+            manual_score_user_id = #{manualScoreUserId},
+            manual_score_target_user_id = #{manualScoreTargetUserId},
+            manual_score_time  = #{manualScoreTime},
+            update_by          = #{updateBy.id},
+            update_date        = #{updateDate}
+        WHERE id = #{id}
+    </update>
+
+    <!-- 清除手动赋分标记(非手动赋分时调用) -->
+    <update id="clearManualScore">
+        UPDATE work_knowledge_base_share_info SET
+            is_manual_score    = '0',
+            manual_score_user_id = NULL,
+            manual_score_target_user_id = NULL,
+            manual_score_time  = NULL,
+            update_by          = #{updateBy.id},
+            update_date        = #{updateDate}
+        WHERE id = #{id}
+    </update>
+
     <!-- 根据文件名称查询重名文件的树节点信息(排除问答答疑分类) -->
     <select id="findDuplicateFilePaths" resultType="java.util.HashMap">
         SELECT DISTINCT
@@ -346,4 +397,12 @@
           </if>
     </select>
 
+    <!-- 根据ID查询并加行锁(SELECT ... FOR UPDATE),用于审核操作的并发控制 -->
+    <select id="getForUpdate" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseShareInfo">
+        SELECT <include refid="shareColumns"/>
+        FROM work_knowledge_base_share_info a
+        WHERE a.id = #{id}
+        FOR UPDATE
+    </select>
+
 </mapper>

+ 17 - 1
src/main/resources/mappings/modules/workprojectnotify/WorkProjectNotifyDao.xml

@@ -83,7 +83,7 @@
 		SELECT <include refid="workProjectNotifyColumns"/>
 		FROM work_project_notify a
 		<include refid="workProjectNotifyJoins"/>
-		WHERE a.notify_id = #{notifyId} AND a.notify_user = #{notifyUserId}
+		WHERE a.notify_id = #{notifyId} AND a.notify_user = #{notifyUserId} and a.del_flag = 0
 	</select>
 
 	<select id="findList" resultType="WorkProjectNotify" >
@@ -663,6 +663,14 @@
 		WHERE notify_id = #{notifyId}
 	</update>
 
+	<update id="updateDelflagByNotifyIdAndUserId">
+		UPDATE work_project_notify SET
+									   update_by = #{updateBy.id},
+									   update_date = #{updateDate},
+									   del_flag = #{delFlag}
+		WHERE notify_id = #{notifyId} AND notify_user = #{user.id}
+	</update>
+
 	<update id="updateReadStateByNotifyId">
 		UPDATE work_project_notify SET
 									   update_by = #{updateBy.id},
@@ -1103,4 +1111,12 @@
 		WHERE a.notify_id = #{notifyId} AND a.notify_user = #{notifyUserId} and a.title= #{title} and a.del_flag = 0 and a.status = 0
 	</select>
 
+	<select id="getListByNotifyId" resultType="WorkProjectNotify" >
+		SELECT
+		<include refid="workProjectNotifyColumns"/>
+		FROM work_project_notify a
+		<include refid="workProjectNotifyJoins"/>
+		WHERE a.notify_id = #{notifyId}
+	</select>
+
 </mapper>

+ 37 - 0
src/main/resources/mysqlDateBase/alter_work_knowledge_base_point_detail_manual_score.sql

@@ -0,0 +1,37 @@
+-- ============================================================
+-- 知识库积分明细表 - 添加手动赋分相关字段
+-- 执行时间:2026-07-06
+-- 说明:用于记录手动赋分的详细信息,便于追溯和统计
+-- ============================================================
+
+-- 1. 添加是否手动赋分标记(0否 1是)
+ALTER TABLE work_knowledge_base_point_detail 
+ADD COLUMN is_manual_score VARCHAR(2) DEFAULT '0' COMMENT '是否手动赋分(0否 1是)';
+
+-- 2. 添加赋分人ID(仅手动赋分时填写)
+ALTER TABLE work_knowledge_base_point_detail 
+ADD COLUMN manual_score_user_id VARCHAR(64) DEFAULT NULL COMMENT '赋分人ID(仅手动赋分时填写)';
+
+-- 3. 添加被赋分人ID(仅手动赋分时填写,与user_id相同)
+ALTER TABLE work_knowledge_base_point_detail 
+ADD COLUMN manual_score_target_user_id VARCHAR(64) DEFAULT NULL COMMENT '被赋分人ID(仅手动赋分时填写,与user_id相同)';
+
+-- 4. 添加赋分时间(仅手动赋分时填写)
+ALTER TABLE work_knowledge_base_point_detail 
+ADD COLUMN manual_score_time DATETIME DEFAULT NULL COMMENT '赋分时间(仅手动赋分时填写)';
+
+-- ============================================================
+-- 索引优化建议(可选)
+-- ============================================================
+
+-- 为快速查询手动赋分记录,可以添加索引
+-- CREATE INDEX idx_manual_score ON work_knowledge_base_point_detail(is_manual_score);
+
+-- 为按赋分人查询,可以添加索引
+-- CREATE INDEX idx_manual_score_user ON work_knowledge_base_point_detail(manual_score_user_id);
+
+-- ============================================================
+-- 数据初始化说明
+-- ============================================================
+-- 对于已存在的积分明细记录,is_manual_score 默认为 '0'(非手动赋分)
+-- 新增的手动赋分记录会自动设置 is_manual_score = '1' 并填充相关字段

+ 5 - 0
src/main/resources/mysqlDateBase/alter_work_knowledge_base_share_info_expert_reviewer.sql

@@ -0,0 +1,5 @@
+-- 为 work_knowledge_base_share_info 表添加主审专家字段
+-- 执行时间:2026-07-06
+
+ALTER TABLE work_knowledge_base_share_info 
+ADD COLUMN expert_reviewer_ids VARCHAR(500) COMMENT '主审专家ID列表(多个用逗号分隔)' AFTER manual_score_time;

+ 76 - 0
src/main/webapp/WEB-INF/tags/sys/selectExpert.tag

@@ -0,0 +1,76 @@
+<%@ tag language="java" pageEncoding="UTF-8"%>
+<%@ include file="/webpage/include/taglib.jsp"%>
+<%@ attribute name="id" type="java.lang.String" required="true" description="编号"%>
+<%@ attribute name="name" type="java.lang.String" required="true" description="隐藏域名称(ID)"%>
+<%@ attribute name="value" type="java.lang.String" required="false" description="隐藏域值(ID)"%>
+<%@ attribute name="labelName" type="java.lang.String" required="true" description="输入框名称(Name)"%>
+<%@ attribute name="labelValue" type="java.lang.String" required="false" description="输入框值(Name)"%>
+<%@ attribute name="title" type="java.lang.String" required="true" description="选择框标题"%>
+<%@ attribute name="cssClass" type="java.lang.String" required="false" description="css样式"%>
+<%@ attribute name="cssStyle" type="java.lang.String" required="false" description="css样式"%>
+<%@ attribute name="disabled" type="java.lang.String" required="false" description="是否限制选择,如果限制,设置为disabled"%>
+<%@ attribute name="dataMsgRequired" type="java.lang.String" required="false" description=""%>
+<%@ attribute name="maxSelect" type="java.lang.Integer" required="false" description="最大选择人数,默认2"%>
+<%@ attribute name="allowClear" type="java.lang.Boolean" required="false" description="是否允许清除"%>
+	<input id="${id}Id" name="${name}" class="${cssClass}" type="hidden" value="${value}"/>
+	<div class="input-group">
+		<input id="${id}Name" name="${labelName}" type="text" value="${labelValue}" data-msg-required="${dataMsgRequired}"
+		class="${cssClass}" style="height:34px;line-height:34px;background-color:#FFF;${cssStyle}" readonly="readonly" placeholder="请选择专家(最多${maxSelect != null ? maxSelect : 2}人)"/>
+		<span class="input-group-btn">
+	        	 <button type="button"  id="${id}Button" class="btn <c:if test="${fn:contains(cssClass, 'input-sm')}"> btn-sm </c:if><c:if test="${fn:contains(cssClass, 'input-lg')}"> btn-lg </c:if>  btn-primary ${disabled}" style="height:34px;"><i class="fa fa-search"></i>
+	             </button>
+		</span>
+    </div>
+	 <label id="${id}Name-error" class="error" for="${id}Name" style="display:none"></label>
+<script type="text/javascript">
+	$("#${id}Button").click(function(){
+		// 是否限制选择,如果限制,设置为disabled
+		if ($("#${id}Button").hasClass("disabled")){
+			return true;
+		}
+		
+		var maxSelect = ${maxSelect != null ? maxSelect : 2};
+		
+		// 正常打开
+		top.layer.open({
+		    type: 2,
+		    area: ['300px', '500px'],
+		    title:"选择${title}",
+		    content: "${ctx}/tag/treeselectReimbur?url="+encodeURIComponent("/sys/office/expertTreeData")+"&checked=true&notAllowSelectParent=false" ,
+		    btn: ['确定', '关闭']
+    	       ,yes: function(index, layero){ 
+						var tree = layero.find("iframe")[0].contentWindow.tree;
+						var ids = [], names = [], nodes = [];
+						
+						// 获取选中的节点
+						nodes = tree.getCheckedNodes(true);
+						
+						for(var i=0; i<nodes.length; i++) {
+							// 过滤掉父节点
+							if (nodes[i].isParent){
+								continue;
+							}
+							ids.push(nodes[i].id);
+							names.push(nodes[i].name);
+						}
+						
+						// 验证选择人数
+						if(ids.length > maxSelect){
+							top.layer.msg("最多只能选择" + maxSelect + "位专家,当前已选择" + ids.length + "位", {icon: 0});
+							return false;
+						}
+						
+						$("#${id}Id").val(ids.join(",").replace(/u_/ig,""));
+						$("#${id}Name").val(names.join(","));
+						$("#${id}Name").focus();
+						top.layer.close(index);
+				    	       },
+    	cancel: function(index){ 
+    	           //关闭弹窗时清空已选择的专家
+    	           $('#${id}Id').val('');
+    	           $('#${id}Name').val('');
+    	       }
+		});
+
+	});
+</script>

+ 139 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseManualScore.jsp

@@ -0,0 +1,139 @@
+<%@ 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}/layui/layui.js"></script>
+    <link rel='stylesheet' type="text/css" href="${ctxStatic}/layui/css/layui.css"/>
+    <script src="${ctxStatic}/common/html/js/script.js"></script>
+    <style>
+        .score-form-container { padding: 20px 30px; }
+        .score-form-container .layui-form-label { width: 100px; }
+        .score-form-container .layui-input-block { margin-left: 130px; }
+        .info-row { padding: 8px 0; color: #666; font-size: 14px; }
+        .info-row label { color: #333; font-weight: bold; margin-right: 8px; }
+    </style>
+    <script type="text/javascript">
+
+        function blurSubmitterId(obj) {
+            var id = $("#targetUserIdId").val();
+            if(undefined != obj.value && null != obj.value && '' != obj.value){
+                $.ajax({
+                    url:'${ctx}/sys/user/getUserByName?name='+obj.value,
+                    type:"post",
+                    success:function(data){
+                        var user = data.body.data;
+                        if(undefined == user || null == user || '' == user){
+                            $("#targetUserIdId").val("");
+                        }else{
+                            if(data.body.data.id != id){
+                                if(undefined != id && null != id && '' != id){
+                                    $("#targetUserIdId").val("");
+                                }
+                            }
+                        }
+                    }
+                });
+            }else{
+                $("#targetUserIdId").val("");
+            }
+        }
+
+        function doSubmit() {
+            var targetUserId = $('#targetUserIdId').val();
+            var score = $('#score').val();
+            var scoreRuleId = $('#scoreRuleId').val();
+
+            if (!targetUserId) {
+                top.layer.msg('请选择被赋分人员', {icon: 0});
+                return false;
+            }
+            if (!score || parseInt(score) <= 0) {
+                top.layer.msg('请输入有效的赋分分数(正整数)', {icon: 0});
+                return false;
+            }
+            if (!scoreRuleId) {
+                top.layer.msg('请选择赋分类型', {icon: 0});
+                return false;
+            }
+
+            $.ajax({
+                type: 'POST',
+                url: '${ctx}/workKnowledgeBase/share/manualScoreSave',
+                data: {
+                    id: '${entity.id}',
+                    targetUserId: targetUserId,
+                    score: score,
+                    scoreRuleId: scoreRuleId
+                },
+                dataType: 'json',
+                success: function(res) {
+                    if (res.success) {
+                        top.layer.msg(res.message || '手动赋分成功', {icon: 1});
+                        setTimeout(function() {
+                            var parentWin = top.layer.getFrameIndex(window.name);
+                            top.layer.close(parentWin);
+                            if (window.parent && window.parent.search) {
+                                window.parent.search();
+                            }
+                        }, 800);
+                    } else {
+                        top.layer.msg(res.message || '操作失败', {icon: 0});
+                    }
+                },
+                error: function() {
+                    top.layer.msg('请求失败', {icon: 2});
+                }
+            });
+            return false;
+        }
+    </script>
+</head>
+<body>
+<div class="score-form-container">
+    <input type="hidden" id="entityId" value="${entity.id}"/>
+
+    <%-- ========== 文件信息 ========== --%>
+    <div class="layui-form-item">
+        <div class="info-row"><label>文件名称:</label><c:out value='${entity.name}'/></div>
+        <div class="info-row"><label>所属类别:</label><c:out value='${treeName}'/></div>
+        <div class="info-row"><label>赋分人:</label>${currentUserName}</div>
+    </div>
+
+    <hr/>
+
+    <%-- ========== 赋分表单 ========== --%>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color:red;">*</span> 被赋分人:</label>
+        <div class="layui-input-block with-icon">
+            <sys:treeselect id="targetUserId" name="targetUserId" value="${defaultTargetUserId}" labelName="targetUserName" labelValue="${defaultTargetUserName}" 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-form-item">
+        <label class="layui-form-label"><span style="color:red;">*</span> 赋分分数:</label>
+        <div class="layui-input-block">
+            <input type="number" id="score" class="layui-input" placeholder="请输入正整数" min="1" step="1"/>
+        </div>
+    </div>
+
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color:red;">*</span> 赋分类型:</label>
+        <div class="layui-input-block">
+            <select id="scoreRuleId" class="layui-input">
+                <option value="">-- 请选择赋分类型 --</option>
+                <c:forEach items="${scoreTypeOptions}" var="opt">
+                    <option value="${opt.id}"><c:out value='${opt.name}'/></option>
+                </c:forEach>
+            </select>
+        </div>
+    </div>
+
+    <div style="color:#999;font-size:12px;padding:10px 0;margin-left:130px;">
+        说明:赋分后文档将直接标记为审核通过,积分将发放给被赋分人。
+    </div>
+</div>
+</body>
+</html>

+ 124 - 62
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareDetail.jsp

@@ -13,76 +13,115 @@
     </style>
     <script type="text/javascript">
         /** 供父页面弹窗按钮回调 */
-        function doSubmit(obj) {
-            var opinion = $('#auditOpinion').val();
-            if (obj == 1) {
-                // 通过:审核意见为空时默认"同意"
-                if (!opinion || opinion.trim() === '') {
-                    opinion = '同意';
-                }
+        var canAuditFlag = ${canAudit != null ? canAudit : false};
+        // 获取URL中的home参数
+        var homeParam = '';
+        (function() {
+            var urlParams = new URLSearchParams(window.location.search);
+            homeParam = urlParams.get('home') || '';
+        })();
+        
+        function doSubmit(obj){
+            console.log('doSubmit called, obj=', obj);
+            if (!canAuditFlag) {
+                top.layer.msg('您没有权限审核此文件', {icon: 2});
+                return false;
+            }
+            
+            // 先验证表单
+            if(validateForm.form()){
+                var opinion = $('#auditOpinion').val();
+                
+                // 先清理之前添加的隐藏字段(防止重复添加)
+                $('#inputForm input[name="auditOpinion"]').remove();
+                $('#inputForm input[name="score"]').remove();
+                $('#inputForm input[name="result"]').remove();
                 
-                // 如果是技术总结、培训心得或典型案例(原创),需要验证评分
-                <c:if test="${treeName == '技术总结' || treeName == '培训心得' || (treeName == '典型案例' && entity.contentAttribute == '0')}">
-                    var scoreValue = $('#scoreValue').val();
-                    if (!scoreValue || scoreValue.trim() === '') {
-                        top.layer.msg('请输入评分分数', {icon: 2});
-                        return false;
+                if (obj == 1) {
+                    // 通过:审核意见为空时默认"同意"
+                    if (!opinion || opinion.trim() === '') {
+                        opinion = '同意';
                     }
-                    var minScore = ${pointRule != null ? pointRule.pointMin : '0'};
-                    var maxScore = ${pointRule != null ? pointRule.pointMax : '0'};
-                    var scoreNum = parseInt(scoreValue);
                     
-                    // 验证是否为正整数
-                    if (isNaN(scoreNum) || scoreNum <= 0) {
-                        top.layer.msg('评分分数必须为正整数', {icon: 2});
-                        return false;
-                    }
+                    // 如果是技术总结、培训心得或典型案例(原创),需要验证评分
+                    <c:if test="${treeName == '技术总结' || treeName == '培训心得' || (treeName == '典型案例' && entity.contentAttribute == '0')}">
+                        var scoreValue = $('#scoreValue').val();
+                        if (!scoreValue || scoreValue.trim() === '') {
+                            parent.layer.msg('请输入评分分数', {icon: 2});
+                            return false;
+                        }
+                        var minScore = ${pointRule != null ? pointRule.pointMin : '0'};
+                        var maxScore = ${pointRule != null ? pointRule.pointMax : '0'};
+                        var scoreNum = parseInt(scoreValue);
+                        
+                        // 验证是否为正整数
+                        if (isNaN(scoreNum) || scoreNum <= 0) {
+                            parent.layer.msg('评分分数必须为正整数', {icon: 2});
+                            return false;
+                        }
+                        
+                        // 验证是否在范围内
+                        if (scoreNum < minScore || scoreNum > maxScore) {
+                            parent.layer.msg('评分分数必须在' + minScore + '-' + maxScore + '之间', {icon: 2});
+                            return false;
+                        }
+                        
+                        // 验证是否为整数(检查是否有小数点)
+                        if (scoreValue.indexOf('.') !== -1) {
+                            parent.layer.msg('评分分数必须为整数,不能包含小数', {icon: 2});
+                            return false;
+                        }
+                    </c:if>
                     
-                    // 验证是否在范围内
-                    if (scoreNum < minScore || scoreNum > maxScore) {
-                        top.layer.msg('评分分数必须在' + minScore + '-' + maxScore + '之间', {icon: 2});
-                        return false;
-                    }
+                    // 设置表单action为新的auditPassForm方法,并添加home和id参数
+                    var formAction = '${ctx}/workKnowledgeBase/share/auditPassForm?id=${entity.id}&home=' + encodeURIComponent(homeParam);
+                    $("#inputForm").attr("action", formAction);
+                    console.log('Form action set to:', formAction);
                     
-                    // 验证是否为整数(检查是否有小数点)
-                    if (scoreValue.indexOf('.') !== -1) {
-                        top.layer.msg('评分分数必须为整数,不能包含小数', {icon: 2});
-                        return false;
-                    }
-                </c:if>
-                
-                var url = '${ctx}/workKnowledgeBase/share/auditPass';
-            } else {
-                // 驳回:审核意见为空时默认"驳回"
-                if (!opinion || opinion.trim() === '') {
-                    opinion = '驳回';
-                }
-                var url = '${ctx}/workKnowledgeBase/share/auditReject';
-            }
-            var success = false;
-            $.ajax({
-                type: 'POST',
-                url: url,
-                async: false,
-                data: { 
-                    id: '${entity.id}', 
-                    auditOpinion: opinion,
+                    // 添加隐藏字段传递审核意见和评分
+                    $('#inputForm').append('<input type="hidden" name="auditOpinion" value="' + escapeHtml(opinion) + '" />');
                     <c:if test="${treeName == '技术总结' || treeName == '培训心得' || (treeName == '典型案例' && entity.contentAttribute == '0')}">
-                    score: $('#scoreValue').val()
+                    $('#inputForm').append('<input type="hidden" name="score" value="' + $('#scoreValue').val() + '" />');
                     </c:if>
-                },
-                success: function(data) {
-                    if (data.success) {
-                        success = true;
-                    } else {
-                        top.layer.msg(data.message || '操作失败', {icon: 2});
+                    
+                    // 提交表单
+                    $("#inputForm").submit();
+                    return true;
+                } else {
+                    // 驳回:审核意见为空时默认"驳回"
+                    if (!opinion || opinion.trim() === '') {
+                        opinion = '驳回';
                     }
-                },
-                error: function() {
-                    top.layer.msg('请求失败,请重试', {icon: 2});
+                    
+                    // 设置表单action为新的auditPassForm方法(驳回也走同一个接口,只是result不同)
+                    var formAction = '${ctx}/workKnowledgeBase/share/auditPassForm?id=${entity.id}&home=' + encodeURIComponent(homeParam);
+                    $("#inputForm").attr("action", formAction);
+                    console.log('Form action set to:', formAction);
+                    
+                    // 添加隐藏字段传递审核意见(驳回时不传score)
+                    $('#inputForm').append('<input type="hidden" name="auditOpinion" value="' + escapeHtml(opinion) + '" />');
+                    $('#inputForm').append('<input type="hidden" name="result" value="2" />');
+                    
+                    // 提交表单
+                    $("#inputForm").submit();
+                    return true;
                 }
-            });
-            return success;
+            }else {
+                parent.layer.msg("信息未填写完整!", {icon: 5});
+            }
+            return false;
+        }
+        
+        // HTML转义函数,防止XSS
+        function escapeHtml(text) {
+            var map = {
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#039;'
+            };
+            return text.replace(/[&<>"']/g, function(m) { return map[m]; });
         }
         $(document).ready(function() {
             validateForm = $('#inputForm').validate({
@@ -227,6 +266,28 @@
 
             <!-- 审核状态(问答答疑分类不显示) -->
             <c:if test="${!isQaCategory}">
+
+                <!-- 主审专家(如果存在) -->
+                <c:if test="${not empty mainExpertNames}">
+                    <div class="layui-item layui-col-sm6 lw6">
+                        <label class="layui-form-label detail-label">主审专家:</label>
+                        <div class="layui-input-block">
+                            <c:set var="expertNameStr" value=""/>
+                            <c:forEach items="${mainExpertNames}" var="name" varStatus="status">
+                                <c:choose>
+                                    <c:when test="${status.last}">
+                                        <c:set var="expertNameStr" value="${expertNameStr}${name}"/>
+                                    </c:when>
+                                    <c:otherwise>
+                                        <c:set var="expertNameStr" value="${expertNameStr}${name}, "/>
+                                    </c:otherwise>
+                                </c:choose>
+                            </c:forEach>
+                            <input type="text" class="form-control layui-input" value="${expertNameStr}" readonly="readonly"/>
+                        </div>
+                    </div>
+                </c:if>
+
             <div class="layui-item layui-col-sm6 lw6">
                 <label class="layui-form-label detail-label">审核状态:</label>
                 <div class="layui-input-block">
@@ -255,6 +316,7 @@
                            style="color:${auditStatusColor};font-weight:bold;"/>
                 </div>
             </div>
+
             </c:if>
 
 
@@ -390,7 +452,7 @@
         <!-- 审核意见区域(审核中和未审核状态下,非创建人可见,且非只读模式) -->
         <c:set var="auditStatusInt" value="${entity.auditStatus != null ? entity.auditStatus : 0}"/>
         <c:set var="isCreator" value="${entity.createBy != null && entity.createBy.id == fns:getUser().id}"/>
-        <c:if test="${(auditStatusInt == 1 || auditStatusInt == 2) && !isCreator && !readOnly}">
+        <c:if test="${(auditStatusInt == 1 || auditStatusInt == 2) && !isCreator && !readOnly && canAudit}">
             <div class="form-group layui-row">
                 <div class="form-group-label"><h2>审核意见</h2></div>
                 <!-- 如果是技术总结或培训心得,显示评分输入框 -->

+ 62 - 21
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareForm.jsp

@@ -137,31 +137,60 @@
                 });
 
 
-                form.on('radio(contentAttributeRadio)', function (event) {
-                    var radioVal = $(this).val();
-                    if (radioVal == "1") {
-                        $(".contentAttribute").show();
-                    } else {
-                        $(".contentAttribute").hide();
-                    }
-                    form.render("radio");
-                });
-            });
+            /** 控制主审专家选择器的显示/隐藏 */
+            function toggleExpertReviewer() {
+                var $expertSection = $('.expert-reviewer-section');
+                if ($expertSection.length === 0) {
+                    return;
+                }
+                
+                var contentAttribute = $('input[name="contentAttribute"]:checked').val();
+                var treeName = '${treeName}';
+                
+                // 技术总结、培训心得:始终显示
+                // 典型案例:只有原创(contentAttribute='0')时显示
+                if (treeName === '技术总结' || treeName === '培训心得' || (treeName === '典型案例' && contentAttribute === '0')) {
+                    $expertSection.show();
+                } else {
+                    $expertSection.hide();
+                    // 隐藏时清空已选择的专家
+                    $('#expertReviewerId').val('');
+                    $('#expertReviewerName').val('');
+                }
+            }
 
+            /** 监听内容属性变化 */
+            form.on('radio(contentAttributeRadio)', function (event) {
+                var radioVal = $(this).val();
+                if (radioVal == "1") {
+                    $(".contentAttribute").show();
+                } else {
+                    $(".contentAttribute").hide();
+                }
+                
+                // 控制主审专家显示/隐藏
+                toggleExpertReviewer();
+                
+                form.render("radio");
             });
-
-        /** 收集动态字段值为JSON数组,写入隐藏域 */
-        function collectDynamicValues() {
-            var values = [];
-            $('.dynamic-field-input').each(function() {
-                var fieldId = $(this).data('field-id');
-                var fieldKey = $(this).data('field-key');
-                var val = $(this).val();
-                values.push({dynamicFieldId: fieldId, fieldKey: fieldKey, fieldValue: val || ''});
+            
+            // 页面加载时初始化主审专家显示状态
+            toggleExpertReviewer();
             });
-            $('#dynamicValuesJson').val(JSON.stringify(values));
-        }
 
+        });
+
+    /** 收集动态字段值为JSON数组,写入隐藏域 */
+    function collectDynamicValues() {
+        var values = [];
+        $('.dynamic-field-input').each(function() {
+            var fieldId = $(this).data('field-id');
+            var fieldKey = $(this).data('field-key');
+            var val = $(this).val();
+            values.push({dynamicFieldId: fieldId, fieldKey: fieldKey, fieldValue: val || ''});
+        });
+        $('#dynamicValuesJson').val(JSON.stringify(values));
+    }
 
     </script>
 </head>
@@ -221,6 +250,18 @@
                 </c:choose>
             </div>
 
+            <!-- 如果是需要专家审核的类型(典型案例(原创)、技术总结、培训心得)。此处可以由创建人选择一到两位专家组成员作为主审人。 人员选择可以采用-->
+            <c:if test="${isQaCategory == false && (treeName == '技术总结' || treeName == '培训心得' || treeName == '典型案例')}">
+                <div class="layui-item layui-col-sm6 lw6 expert-reviewer-section">
+                    <label class="layui-form-label">主审专家:</label>
+                    <div class="layui-input-block with-icon">
+                        <sys:selectExpert id="expertReviewer" name="expertReviewerIds" value="${entity.expertReviewerIds}" labelName="expertReviewerNames" labelValue="${entity.expertReviewerNames}"
+                                         title="专家" cssClass="form-control judgment layui-input" maxSelect="2"/>
+                        <span class="help-inline" style="color: #999; font-size: 12px;">可选可不选,最多选择2位专家</span>
+                    </div>
+                </div>
+            </c:if>
+
             <!-- 动态扩展字段(与主表单整合,流式排列) -->
             <c:if test="${not empty dynamicFields}">
                 <div class="form-group layui-row">

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

@@ -235,20 +235,22 @@
                 title: '审核详情',
                 skin: 'three-btns',
                 maxmin: true,
-                content: '${ctx}/workKnowledgeBase/share/detail?id=' + id + '&treeNodeId=${treeNodeId}&rootId=${rootId}',
+                content: '${ctx}/workKnowledgeBase/share/detail?id=' + id + '&treeNodeId=${treeNodeId}&rootId=${rootId}&home=knowledgeList',
                 btn: ['通过', '驳回', '关闭'],
                 btn1: function(index, layero) {
                     var iframeWin = layero.find('iframe')[0];
                     if (iframeWin.contentWindow.doSubmit(1)) {
                         top.layer.close(index);
-                        setTimeout(function(){ search(); }, 100);
+                        // 延迟刷新列表页(等待后端处理完成)
+                        setTimeout(function(){ search(); }, 300);
                     }
                 },
                 btn2: function(index, layero) {
                     var iframeWin = layero.find('iframe')[0];
                     if (iframeWin.contentWindow.doSubmit(2)) {
                         top.layer.close(index);
-                        setTimeout(function(){ search(); }, 100);
+                        // 延迟刷新列表页(等待后端处理完成)
+                        setTimeout(function(){ search(); }, 300);
                     }
                     return false;
                 },
@@ -304,6 +306,26 @@
             });
         }
 
+        /** 手动赋分弹窗 */
+        function openManualScoreDialog(id) {
+            top.layer.open({
+                type: 2,
+                area: ['600px', '500px'],
+                title: '手动赋分',
+                maxmin: false,
+                content: '${ctx}/workKnowledgeBase/share/manualScoreForm?id=' + id,
+                skin: 'three-btns',
+                btn: ['保存', '关闭'],
+                btn1: function(index, layero) {
+                    var iframeWin = layero.find('iframe')[0];
+                    if (iframeWin.contentWindow.doSubmit()) {
+                        // doSubmit 内部已处理关闭和刷新
+                    }
+                },
+                btn2: function(index) { top.layer.close(index); }
+            });
+        }
+
         /** 发起审核 */
         function submitAudit(id) {
             layer.open({
@@ -607,6 +629,7 @@
         function resetSearch() {
             $('#searchForm input[type=text]').val('');
             $('#searchForm input[type=search]').val('');
+            $('#searchForm select').val('');
             $('#searchForm').submit();
         }
 
@@ -667,6 +690,19 @@
 
                     <%-- 展开区域:动态字段查询(默认隐藏) --%>
                     <div id="moresees" style="clear:both;display:none;" class="lw7">
+
+
+                        <div class="layui-item query athird" style="clear:both;">
+                            <label class="layui-form-label">是否手动赋分:</label>
+                            <div class="layui-input-block with-icon">
+                                <select name="isManualScore" class="form-control simple-select">
+                                    <option value=""></option>
+                                    <option value="1" ${shareInfo.isManualScore == '1' ? 'selected' : ''}>是</option>
+                                    <option value="0" ${shareInfo.isManualScore == '0' ? 'selected' : ''}>否</option>
+                                </select>
+                            </div>
+                        </div>
+
                         <c:forEach items="${dynamicFields}" var="field">
                             <div class="layui-item query athird">
                                 <c:choose>
@@ -837,14 +873,8 @@
                     ? (d.submitAuditUserId === d.userid) 
                     : (d.createbyid === d.userid);
 
-                // 判断是否为需要专家审核的特殊分类
-                // 技术总结/培训心得:需要专家审核
-                // 典型案例(原创)contentAttribute=0:需要专家审核
-                // 典型案例(转载)contentAttribute=1:所有人可审核
-                var needsExpertAudit = (d.treeName === '技术总结' || d.treeName === '培训心得')
-                    || (d.treeName === '典型案例' && d.contentAttribute === '0');
-                // 判断当前用户是否有审核权限(管理员、专家或普通分类)
-                var canAudit = !isCreator && !d.currentUserAudited && (d.isAdmin || d.isExpert || !needsExpertAudit);
+                // 直接使用后端计算的canAudit值(包含主审专家抢办名额精确计算)
+                var canAudit = d.canAudit === true;
             
                 // ========== 问答答疑分类特殊处理 ==========
                 if (d.isQaCategory) {
@@ -931,6 +961,13 @@
                 if (d.canAdjustCategory === '1') {
                     xml += '<a href="javascript:void(0)" onclick="openChangeCategoryDialog(\'' + d.id + '\')" class="layui-btn layui-btn-xs layui-bg-cyan"> 调整分类</a>';
                 }
+
+                //添加手动赋分功能(主要是为了有些文章不适合审批,上传人可能都不是文档原创的本人。上传之后,由具有相关权限的人直接进行赋分,赋分后文档相当于直接审核通过)
+                <shiro:hasPermission name="workKnowledgeBase:share:audit">
+                if (d.canManualScore === '1' && d.auditStatus !== 5 && !d.isQaCategory) {
+                    xml += '<a href="javascript:void(0)" onclick="openManualScoreDialog(\'' + d.id + '\')" class="layui-btn layui-btn-xs layui-bg-green"> 手动赋分</a>';
+                }
+                </shiro:hasPermission>
             
                 xml += '</div>';
                 return xml;
@@ -982,7 +1019,10 @@
                     </c:if>
                     </c:forEach>
                     ,"currentUserAudited": ${row.currentUserAudited == true}
+                    ,"canAudit": ${row.canAudit == true}
+                    ,"expertReviewerIds": "<c:out value='${row.expertReviewerIds}'/>"
                     <shiro:hasPermission name="workKnowledgeBase:share:adjustCategory">,"canAdjustCategory":"1"</shiro:hasPermission>
+                    <shiro:hasPermission name="workKnowledgeBase:share:audit">,"canManualScore":"1"</shiro:hasPermission>
                 }
                 </c:forEach>
                 </c:when>

+ 3 - 0
src/main/webapp/webpage/modules/sys/sysHome.jsp

@@ -160,7 +160,10 @@
                         top_iframe = top.getActiveTab().attr("name");//获取当前active的tab的iframe
                     }
                     inputForm.attr("target",top_iframe);//表单提交成功后,从服务器返回的url在当前tab中展示
+                    // 将目标iframe名称传给子页面,供审核成功后刷新列表页
+                    iframeWin.contentWindow._targetIframe = top_iframe;
                     if(iframeWin.contentWindow.doSubmit(1,index) ){
+                        console.log("执行完成")
                         top.layer.close(index);//关闭对话框。
                         setTimeout(function(){top.layer.close(index)}, 100);//延时0.1秒,对应360 7.1版本bug
                     }

+ 61 - 0
src/main/webapp/webpage/modules/workprojectnotify/workProjectNotifyList.jsp

@@ -631,6 +631,61 @@
 			}
 
 		}
+
+
+		/** 打开确认最佳答案页面(带提交/关闭按钮) */
+		function openConfirmSpecialistWorkKnowledge(title,url,width,height,target){
+			if(navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)){//如果是移动端,就使用自适应大小弹窗
+				width='auto';
+				height='auto';
+			}else{//如果是PC端,根据用户设置的width和height显示。
+
+			}
+			top.layer.open({
+				type: 2,
+				area: [width, height],
+				title: title,
+				skin:"three-btns",
+				maxmin: true, //开启最大化最小化按钮
+				content: url ,
+				btn: ['通过','驳回','关闭'],
+				btn1: function(index, layero){
+					var body = top.layer.getChildFrame('body', index);
+					var iframeWin = layero.find('iframe')[0]; //得到iframe页的窗口对象,执行iframe页的方法:iframeWin.method();
+					var inputForm = body.find('#inputForm');
+					var top_iframe;
+					if(target){
+						top_iframe = target;//如果指定了iframe,则在改frame中跳转
+					}else{
+						top_iframe = top.getActiveTab().attr("name");//获取当前active的tab的iframe
+					}
+					inputForm.attr("target",top_iframe);//表单提交成功后,从服务器返回的url在当前tab中展示
+					if(iframeWin.contentWindow.doSubmit(1) ){
+						top.layer.close(index);//关闭对话框。
+						setTimeout(function(){top.layer.close(index)}, 100);//延时0.1秒,对应360 7.1版本bug
+					}
+				},
+				btn2:function(index,layero){
+					var body = top.layer.getChildFrame('body', index);
+					var iframeWin = layero.find('iframe')[0]; //得到iframe页的窗口对象,执行iframe页的方法:iframeWin.method();
+					var inputForm = body.find('#inputForm');
+					var top_iframe;
+					if(target){
+						top_iframe = target;//如果指定了iframe,则在改frame中跳转
+					}else{
+						top_iframe = top.getActiveTab().attr("name");//获取当前active的tab的iframe
+					}
+					inputForm.attr("target",top_iframe);//表单提交成功后,从服务器返回的url在当前tab中展示
+					if(iframeWin.contentWindow.doSubmit(2) ){
+						top.layer.close(index);//关闭对话框。
+						setTimeout(function(){top.layer.close(index)}, 100);//延时0.1秒,对应360 7.1版本bug
+					}
+					return false;
+				},
+				btn3: function(index){
+				}
+			});
+		}
 	</script>
 	<style>
 		body{
@@ -1254,6 +1309,12 @@
 						}
 
                     }
+					else if(d.type == "200"){
+						return "<a class=\"attention-info\" href=\"javascript:void(0)\" onclick=\"openConfirmSpecialistWorkKnowledge('"+ d.type1 +"待审批', '${ctx}/workprojectnotify/workProjectNotify/form?id="+d.id+"&home=notifyList','95%','95%')\">" +
+								"<span title=\""+ d.title +"\">"+ d.title +"</span>" +
+								"</a>";
+
+					}
 
                     else if(d.remarks == "待审批" && d.status != "1") {
                     	if (d.isreply == "1"){

+ 6 - 0
src/main/webapp/webpage/modules/workprojectnotify/workProjectNotifyReadShowList.jsp

@@ -411,6 +411,12 @@
 									"</a>";
 
 						}
+						else if(d.type == "200"){
+							return "<a class=\"attention-info\"  href=\"javascript:void(0)\" onclick=\"openDialogView('查看通知', '${ctx}/workprojectnotify/workProjectNotify/form?id="+d.id+"','95%','95%')\">" +
+									"<span title=\""+ d.title +"\">"+ d.title +"</span>" +
+									"</a>";
+
+						}
                         else{
                         	if (d.isreply == "2"){
 								return "<a class=\"attention-info\"  href=\"javascript:void(0)\" onclick=\"openDialogView('查看通知', '${ctx}/workprojectnotify/workProjectNotify/form?id="+d.id+"','95%','95%')\">" +