Bladeren bron

知识库积分部分功能提交,问答答疑部分最佳回答由一条调整为上限5条功能变更

徐滕 1 dag geleden
bovenliggende
commit
beb5e6fb07

+ 276 - 0
docs/确认最佳答案功能优化说明.md

@@ -0,0 +1,276 @@
+# 确认最佳答案功能优化说明
+
+## 修改概述
+
+按照您的要求,重新设计了"确认最佳答案"功能的交互方式,采用带提交/关闭按钮的弹窗模式,并优化了选择按钮的样式和交互体验。
+
+## 核心改进
+
+### 1. 独立的确认页面
+**文件**: `workKnowledgeBaseConfirmAnswer.jsp`
+
+**特点**:
+- ✅ 专门的页面用于选择最佳答案
+- ✅ 底部固定操作栏(显示已选数量和提交/关闭按钮)
+- ✅ 美观的回答卡片式布局
+- ✅ 按钮样式交互(点击变色、文字切换)
+
+### 2. 弹窗模式
+**参考样式**: 使用 `skin: 'three-btns'` 和双按钮配置
+
+**按钮**:
+- **提交**: 调用 iframe 内的 `doSubmit()` 方法
+- **关闭**: 直接关闭弹窗
+
+### 3. 交互优化
+
+#### 选择按钮样式
+```css
+.select-best-btn {
+    /* 未选中状态 */
+    border: 1px solid #d9d9d9;
+    color: #666;
+    background: white;
+}
+
+.select-best-btn.selected {
+    /* 选中状态 - 浅橙色 */
+    background-color: #ff9800;
+    color: white;
+    border-color: #ff9800;
+}
+```
+
+#### 按钮文字切换
+- **未选中**: "选择为最佳答案"(灰色边框)
+- **已选中**: "取消选择"(浅橙色背景,白色文字)
+
+#### 数量限制反馈
+- 实时显示:`已选择 X/5 条`
+- 超过5条时弹出提示:`最多只能选择5个最佳答案`
+- 提交按钮自动启用/禁用
+
+## 文件清单
+
+### 新增文件
+1. **workKnowledgeBaseConfirmAnswer.jsp**
+   - 路径: `src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseConfirmAnswer.jsp`
+   - 用途: 确认最佳答案专用页面
+
+### 修改文件
+1. **WorkKnowledgeBaseShareController.java**
+   - 新增方法: `confirmAnswerPage()`
+   - 用途: 提供确认页面的访问入口
+
+2. **workKnowledgeBaseShareList.jsp**
+   - 新增方法: `openConfirmAnswerDialog()`
+   - 修改按钮: "确认答案"按钮调用新方法
+
+## 功能流程
+
+### 1. 打开确认页面
+```javascript
+// 列表页点击"确认答案"按钮
+openConfirmAnswerDialog(questionId)
+  ↓
+// 打开弹窗,加载确认页面
+top.layer.open({
+    content: '${ctx}/workKnowledgeBase/share/confirmAnswerPage?id=' + id,
+    btn: ['提交', '关闭']
+})
+```
+
+### 2. 选择最佳答案
+```javascript
+// 用户点击选择按钮
+toggleSelect(btn)
+  ↓
+// 判断是否已选中
+if (已选中) {
+    取消选择,文字变为"选择为最佳答案"
+} else {
+    if (已选数量 >= 5) {
+        弹出提示
+    } else {
+        选中,背景变橙色,文字变为"取消选择"
+    }
+}
+  ↓
+// 更新界面状态
+updateUI()
+  - 更新已选数量显示
+  - 启用/禁用提交按钮
+```
+
+### 3. 提交确认
+```javascript
+// 用户点击"提交确认"按钮
+doSubmit()
+  ↓
+// 验证至少选择1条
+if (selectedAnswers.length === 0) {
+    提示"请至少选择一个答案"
+    return false;
+}
+  ↓
+// 弹出确认对话框
+layer.confirm('确认将这 X 条回答设为最佳答案并提交吗?')
+  ↓
+// 调用后端接口
+$.ajax({
+    url: '${ctx}/workKnowledgeBase/share/confirmMultipleBestAnswers',
+    data: { questionId, answerIds }
+})
+  ↓
+// 处理结果
+if (success) {
+    提示"提交成功,积分已发放"
+    关闭弹窗
+    刷新列表
+}
+```
+
+## 技术要点
+
+### 1. 弹窗父子通信
+```javascript
+// 父窗口调用子窗口方法
+btn1: function(index, layero) {
+    var iframeWin = layero.find('iframe')[0];
+    if (iframeWin.contentWindow.doSubmit()) {
+        // 提交成功后关闭弹窗
+        top.layer.close(index);
+    }
+}
+
+// 子窗口暴露方法给父窗口
+window.doSubmit = doSubmit;
+```
+
+### 2. 数组参数传递
+```javascript
+// 使用 traditional: true 确保数组正确传递
+$.ajax({
+    data: { answerIds: selectedAnswers },
+    traditional: true  // 重要!
+})
+```
+
+### 3. 实时状态管理
+```javascript
+var selectedAnswers = [];  // 存储已选答案ID
+
+// 选择时添加
+selectedAnswers.push(answerId);
+
+// 取消时过滤
+selectedAnswers = selectedAnswers.filter(id => id !== answerId);
+
+// 更新UI
+$('#selectedCount').text(selectedAnswers.length);
+```
+
+## 样式设计
+
+### 回答卡片
+```css
+.answer-item {
+    background: white;
+    padding: 15px;
+    margin-bottom: 15px;
+    border-radius: 5px;
+    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+}
+```
+
+### 底部固定栏
+```css
+.footer-bar {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    background: white;
+    padding: 15px;
+    box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
+    z-index: 999;
+}
+```
+
+### 选择按钮交互
+```css
+/* 悬停效果 */
+.select-best-btn:hover {
+    color: #ff9800;
+    border-color: #ff9800;
+}
+
+/* 选中状态 */
+.select-best-btn.selected {
+    background-color: #ff9800;
+    color: white;
+}
+```
+
+## 用户体验提升
+
+### 对比旧版
+| 项目 | 旧版 | 新版 |
+|------|------|------|
+| 选择方式 | 小复选框 | 大按钮,点击变色 |
+| 视觉反馈 | 无明显变化 | 浅橙色高亮,文字切换 |
+| 操作位置 | 顶部小框 | 底部固定栏,符合习惯 |
+| 提交方式 | 即时确认 | 统一提交,二次确认 |
+| 数量提示 | 静态文本 | 实时更新,动态禁用按钮 |
+
+### 新版优势
+1. **更直观**: 大按钮比小复选框更容易点击
+2. **更清晰**: 颜色变化和文字切换提供明确的视觉反馈
+3. **更安全**: 底部统一提交,避免误操作
+4. **更美观**: 卡片式布局,整洁大方
+5. **更友好**: 实时计数和动态按钮状态
+
+## 注意事项
+
+### 1. 权限验证
+- Controller 层验证:只有问题发起人可以访问
+- 前端也进行了身份判断,双重保障
+
+### 2. 状态检查
+- 问题已结束(auditStatus=8)不允许再次确认
+- 已选择的答案在提交时会再次验证
+
+### 3. 数据一致性
+- 后端 Service 层会验证每个答案的有效性
+- 自动去重回答人ID进行积分发放
+- 同一人的多条答案被选中时会多次赋分
+
+### 4. 浏览器兼容
+- 使用了 ES5 语法,兼容主流浏览器
+- 移动端自适应(width/height = 'auto')
+
+## 测试建议
+
+### 功能测试
+- [ ] 选择1-5条答案并成功提交
+- [ ] 尝试选择6条,验证限制提示
+- [ ] 不选择任何答案,验证提交按钮禁用
+- [ ] 选中后取消,验证状态正确切换
+- [ ] 非发起人尝试访问,验证权限拦截
+
+### 界面测试
+- [ ] 不同分辨率下的显示效果
+- [ ] 移动端适配
+- [ ] 底部固定栏是否正常显示
+- [ ] 按钮颜色和文字是否正确
+
+### 集成测试
+- [ ] 提交成功后弹窗是否正常关闭
+- [ ] 列表是否正确刷新
+- [ ] 积分是否正确发放(特别是同一人多条的情况)
+
+---
+
+**完成日期**: 2026-01-16  
+**版本**: v2.0  
+**作者**: AI Assistant

+ 338 - 0
docs/问答答疑多选最佳答案功能实现说明.md

@@ -0,0 +1,338 @@
+# 问答答疑多选最佳答案功能实现说明
+
+## 功能概述
+
+将原有的单选最佳答案功能升级为多选最佳答案功能,支持以下特性:
+- ✅ 最多可选择5条内容作为最佳答案
+- ✅ 五条内容可以是同一人
+- ✅ 每个内容均进行赋分
+- ✅ 不限制每个人只能获得1次的分数
+- ✅ 统一提交后对回答人和提问人员进行赋分
+
+## 数据库变更
+
+### 1. 新建表: work_knowledge_base_best_answer
+
+**文件**: `alter_work_knowledge_base_add_best_answers.sql`
+
+```sql
+CREATE TABLE IF NOT EXISTS work_knowledge_base_best_answer (
+    id VARCHAR(64) NOT NULL COMMENT '主键ID',
+    question_id VARCHAR(64) NOT NULL COMMENT '问题ID',
+    answer_id VARCHAR(64) NOT NULL COMMENT '答案ID',
+    sort_order INT DEFAULT 0 COMMENT '排序号',
+    create_by VARCHAR(64) COMMENT '创建人ID',
+    create_date DATETIME COMMENT '创建时间',
+    del_flag CHAR(1) DEFAULT '0' COMMENT '删除标记',
+    PRIMARY KEY (id),
+    KEY idx_question_id (question_id),
+    KEY idx_answer_id (answer_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问答最佳答案关联表';
+```
+
+**说明**: 
+- 原`work_knowledge_base_share_info`表中的`answer_id`字段保留但不再使用
+- 新表支持一对多关系,一个问题可对应多条最佳答案
+
+## 后端实现
+
+### 1. 实体类 - WorkKnowledgeBaseBestAnswer.java
+
+**位置**: `src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseBestAnswer.java`
+
+**主要字段**:
+- `questionId`: 问题ID
+- `answerId`: 答案ID
+- `sortOrder`: 排序号
+- `answerContent`: 回答内容(关联查询)
+- `answerUserName`: 回答人姓名(关联查询)
+- `answerUserId`: 回答人ID(关联查询)
+
+### 2. DAO层 - WorkKnowledgeBaseBestAnswerDao.java
+
+**位置**: `src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseBestAnswerDao.java`
+
+**主要方法**:
+- `findByQuestionId()`: 根据问题ID查询最佳答案列表
+- `countByQuestionId()`: 统计问题的最佳答案数量
+- `isBestAnswer()`: 检查答案是否已被设为最佳答案
+- `batchInsert()`: 批量插入最佳答案
+- `deleteByQuestionId()`: 删除问题的所有最佳答案
+
+### 3. MyBatis映射 - WorkKnowledgeBaseBestAnswerDao.xml
+
+**位置**: `src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseBestAnswerDao.xml`
+
+**关键SQL**:
+- 关联查询最佳答案及回答内容、回答人信息
+- 批量插入最佳答案记录
+
+### 4. Service层 - WorkKnowledgeBaseShareService.java
+
+#### 新增方法:
+
+**confirmMultipleBestAnswers(String questionId, List<String> answerIds)**
+```java
+/**
+ * 确认多个最佳答案(最多5条)
+ * @param questionId 问题ID
+ * @param answerIds 答案ID列表
+ * @return 返回所有回答人ID列表(不重复)
+ */
+```
+
+**核心逻辑**:
+1. 验证参数(至少1条,最多5条)
+2. 检查问题状态和分类
+3. 验证每个答案是否存在且属于该问题
+4. 删除旧的最佳答案记录
+5. 批量插入新的最佳答案记录
+6. 更新问题状态为已结束(8)
+7. 收集并返回所有不重复的回答人ID
+
+**findBestAnswers(String questionId)**
+- 查询问题的最佳答案列表
+
+**isBestAnswer(String questionId, String answerId)**
+- 检查答案是否已被设为最佳答案
+
+#### 修改方法:
+
+**grantQaPointsToMultipleUsers(String questionId, List<String> answerUserIds)**
+```java
+/**
+ * 给问答参与人员发放积分(支持多个回答人)
+ * @param questionId 问题ID
+ * @param answerUserIds 所有回答人ID列表(不重复)
+ */
+```
+
+**核心逻辑**:
+1. 查询"问答答疑"分类配置
+2. 获取提问积分规则和回答积分规则
+3. 给问题发起人发放积分(一次)
+4. **遍历每个回答人发放积分**(同一个人如果有多条最佳答案,会多次发放)
+
+### 5. Controller层 - WorkKnowledgeBaseShareController.java
+
+#### 修改接口:
+
+**confirmMultipleBestAnswers()**
+```java
+@RequestMapping(value = "confirmMultipleBestAnswers")
+@ResponseBody
+public Map<String, Object> confirmMultipleBestAnswers(
+    @RequestParam String questionId,
+    @RequestParam List<String> answerIds)
+```
+
+**处理流程**:
+1. 验证是否为问题发起人
+2. 调用Service保存多条最佳答案
+3. 调用Service发放积分给所有回答人
+4. 返回成功信息和回答人数量
+
+## 前端实现
+
+### 1. 详情页 - workKnowledgeBaseShareDetail.jsp
+
+#### UI变更:
+
+**新增确认区域**:
+```html
+<div id="confirmBestAnswersArea" style="display:none;">
+    <div style="text-align:right;">
+        <span>已选择 <strong id="selectedCount">0</strong>/5 条</span>
+        <button id="batchConfirmBtn" disabled>确认选择的答案</button>
+    </div>
+</div>
+```
+
+**回答项变更**:
+- 原"确认为最佳答案"链接改为复选框
+- 复选框样式: `<input type="checkbox" class="best-answer-checkbox">`
+- 仅问题发起人且未确认答案时显示复选框
+
+#### JavaScript变更:
+
+**新增变量**:
+```javascript
+var hasConfirmedAnswers = false; // 是否已确认答案
+```
+
+**新增函数**:
+
+**bindCheckboxEvents()**
+- 监听复选框变化
+- 限制最多选择5条
+- 实时更新已选择数量
+- 启用/禁用确认按钮
+
+**batchConfirmBestAnswers()**
+- 收集所有选中的答案ID
+- 调用后端API批量确认
+- 成功后刷新列表并隐藏确认区域
+
+**修改函数**:
+
+**renderAnswers()**
+- 渲染完成后绑定复选框事件
+- 显示确认区域(仅问题发起人)
+- 更新`hasConfirmedAnswers`全局变量
+
+**renderAnswerItem()**
+- 将"确认最佳答案"链接改为复选框
+- 条件判断使用`hasConfirmedAnswers`替代`hasBestAnswer`
+
+### 2. 列表页 - workKnowledgeBaseShareList.jsp
+
+#### 变更:
+
+**确认答案按钮**:
+- 原来: `openDetailDialogWithRead()` - 只读模式
+- 现在: `openQaDetailDialogWithRead()` - 带确认功能的只读模式
+- 目的: 打开详情页让用户选择多个最佳答案
+
+## 积分发放规则
+
+### 发放时机
+- 问题发起人确认最佳答案后立即发放
+
+### 发放对象
+
+**1. 问题发起人**
+- 发放一次提问积分(rule_type=1)
+- 从"问答答疑"分类的积分规则中读取固定分值
+
+**2. 回答人**
+- 每条最佳答案都发放回答积分(rule_type=2)
+- **五条内容可以是同一人**
+- **每个内容均进行赋分**
+- **不限制每个人只能获得1次的分数**
+
+### 示例场景
+
+**场景1**: 5条最佳答案来自5个不同的人
+- 问题发起人获得1次提问积分
+- 5个回答人各获得1次回答积分
+
+**场景2**: 5条最佳答案全部来自同一个人
+- 问题发起人获得1次提问积分
+- 该回答人获得5次回答积分(5 × 单次积分值)
+
+**场景3**: 3条最佳答案,其中2条来自A,1条来自B
+- 问题发起人获得1次提问积分
+- A获得2次回答积分
+- B获得1次回答积分
+
+## 使用说明
+
+### 操作流程
+
+1. **问题发起人**点击"确认答案"按钮
+2. 系统打开详情页,显示所有回答
+3. 每个回答右侧显示复选框"选择为最佳答案"
+4. 发起人勾选最多5条最佳答案
+5. 页面实时显示已选择数量(X/5)
+6. 点击"确认选择的答案"按钮
+7. 系统提示确认信息
+8. 确认后:
+   - 问题状态变为"已结束"(8)
+   - 最佳答案被标记
+   - 积分自动发放
+   - 其他人无法继续回答
+
+### 限制条件
+
+- ✅ 只有问题发起人可以确认答案
+- ✅ 最多选择5条最佳答案
+- ✅ 至少选择1条最佳答案
+- ✅ 确认后不可撤销
+- ✅ 问题结束后无法继续回答
+
+## 技术要点
+
+### 1. 数据去重
+- Service层使用`HashSet`收集回答人ID
+- 确保即使同一人有多条最佳答案,在列表中只出现一次
+- 但积分发放时会遍历原始答案列表,保证多次发放
+
+### 2. 事务控制
+- `@Transactional(readOnly = false)`确保原子性
+- 最佳答案保存和积分发放要么全部成功,要么全部失败
+
+### 3. 批量操作
+- 使用MyBatis的`<foreach>`标签实现批量插入
+- 减少数据库交互次数,提高性能
+
+### 4. 前端交互
+- 使用jQuery事件委托(`on('change')`)绑定动态元素
+- 实时反馈用户操作(计数更新、按钮启用/禁用)
+- Layer弹窗提供友好的确认提示
+
+## 测试建议
+
+### 功能测试
+
+1. **基本功能**
+   - [ ] 选择1-5条最佳答案并确认
+   - [ ] 尝试选择6条,验证限制
+   - [ ] 不选择任何答案,验证提示
+
+2. **积分发放**
+   - [ ] 5条答案来自5个人,验证每人获得1次积分
+   - [ ] 5条答案来自1个人,验证该人获得5次积分
+   - [ ] 混合场景,验证积分正确发放
+
+3. **边界情况**
+   - [ ] 问题已确认后再次确认,验证拦截
+   - [ ] 非发起人尝试确认,验证权限
+   - [ ] 只读模式打开,验证复选框不显示
+
+### 性能测试
+
+- [ ] 大量答案场景下的选择性能
+- [ ] 批量插入5条记录的数据库性能
+- [ ] 并发确认同一问题的锁机制
+
+## 注意事项
+
+1. **数据库迁移**
+   - 执行SQL脚本前备份数据
+   - 建议在测试环境先验证
+
+2. **兼容性**
+   - 旧的单条最佳答案数据仍然保留在`answer_id`字段
+   - 新数据存储在关联表中
+   - 后续可编写迁移脚本将旧数据迁移到新表
+
+3. **日志记录**
+   - 建议在Service层添加操作日志
+   - 记录谁在何时确认了哪些答案为最佳
+
+4. **权限控制**
+   - 已有`@RequiresPermissions`注解保护
+   - 前端也进行了发起人身份验证
+   - 双重保障确保安全
+
+## 相关文件清单
+
+### 后端文件
+- `WorkKnowledgeBaseBestAnswer.java` - 实体类
+- `WorkKnowledgeBaseBestAnswerDao.java` - DAO接口
+- `WorkKnowledgeBaseBestAnswerDao.xml` - MyBatis映射
+- `WorkKnowledgeBaseShareService.java` - Service层(修改)
+- `WorkKnowledgeBaseShareController.java` - Controller层(修改)
+
+### 前端文件
+- `workKnowledgeBaseShareDetail.jsp` - 详情页(修改)
+- `workKnowledgeBaseShareList.jsp` - 列表页(修改)
+
+### 数据库脚本
+- `alter_work_knowledge_base_add_best_answers.sql` - 建表脚本
+
+---
+
+**完成日期**: 2026-01-16  
+**版本**: v1.0  
+**作者**: AI Assistant

+ 53 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/dao/WorkKnowledgeBaseBestAnswerDao.java

@@ -0,0 +1,53 @@
+package com.jeeplus.modules.WorkKnowledgeBase.dao;
+
+import com.jeeplus.common.persistence.CrudDao;
+import com.jeeplus.common.persistence.annotation.MyBatisDao;
+import com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseBestAnswer;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 问答最佳答案关联表 DAO
+ * @author system
+ * @version 2026-01-16
+ */
+@MyBatisDao
+public interface WorkKnowledgeBaseBestAnswerDao extends CrudDao<WorkKnowledgeBaseBestAnswer> {
+    
+    /**
+     * 根据问题ID查询最佳答案列表
+     * @param questionId 问题ID
+     * @return 最佳答案列表(按排序号排序)
+     */
+    List<WorkKnowledgeBaseBestAnswer> findByQuestionId(@Param("questionId") String questionId);
+    
+    /**
+     * 统计问题的最佳答案数量
+     * @param questionId 问题ID
+     * @return 最佳答案数量
+     */
+    int countByQuestionId(@Param("questionId") String questionId);
+    
+    /**
+     * 检查答案是否已被设为最佳答案
+     * @param questionId 问题ID
+     * @param answerId 答案ID
+     * @return true-已是最佳答案,false-不是
+     */
+    boolean isBestAnswer(@Param("questionId") String questionId, @Param("answerId") String answerId);
+    
+    /**
+     * 批量插入最佳答案
+     * @param bestAnswers 最佳答案列表
+     * @return 插入的记录数
+     */
+    int batchInsert(List<WorkKnowledgeBaseBestAnswer> bestAnswers);
+    
+    /**
+     * 删除问题的所有最佳答案
+     * @param questionId 问题ID
+     * @return 删除的记录数
+     */
+    int deleteByQuestionId(@Param("questionId") String questionId);
+}

+ 78 - 0
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/entity/WorkKnowledgeBaseBestAnswer.java

@@ -0,0 +1,78 @@
+package com.jeeplus.modules.WorkKnowledgeBase.entity;
+
+import com.jeeplus.common.persistence.DataEntity;
+
+/**
+ * 问答最佳答案关联表 Entity
+ * @author system
+ * @version 2026-01-16
+ */
+public class WorkKnowledgeBaseBestAnswer extends DataEntity<WorkKnowledgeBaseBestAnswer> {
+    
+    private static final long serialVersionUID = 1L;
+    
+    private String questionId;        // 问题ID
+    private String answerId;          // 答案ID
+    private Integer sortOrder;        // 排序号
+    
+    // 查询条件(用于关联查询)
+    private String answerContent;     // 回答内容
+    private String answerUserName;    // 回答人姓名
+    private String answerUserId;      // 回答人ID
+
+    public WorkKnowledgeBaseBestAnswer() {
+        super();
+    }
+
+    public WorkKnowledgeBaseBestAnswer(String id) {
+        super(id);
+    }
+
+    public String getQuestionId() {
+        return questionId;
+    }
+
+    public void setQuestionId(String questionId) {
+        this.questionId = questionId;
+    }
+
+    public String getAnswerId() {
+        return answerId;
+    }
+
+    public void setAnswerId(String answerId) {
+        this.answerId = answerId;
+    }
+
+    public Integer getSortOrder() {
+        return sortOrder;
+    }
+
+    public void setSortOrder(Integer sortOrder) {
+        this.sortOrder = sortOrder;
+    }
+
+    public String getAnswerContent() {
+        return answerContent;
+    }
+
+    public void setAnswerContent(String answerContent) {
+        this.answerContent = answerContent;
+    }
+
+    public String getAnswerUserName() {
+        return answerUserName;
+    }
+
+    public void setAnswerUserName(String answerUserName) {
+        this.answerUserName = answerUserName;
+    }
+
+    public String getAnswerUserId() {
+        return answerUserId;
+    }
+
+    public void setAnswerUserId(String answerUserId) {
+        this.answerUserId = answerUserId;
+    }
+}

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

@@ -98,6 +98,9 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     @Autowired
     private WorkKnowledgeBaseQaAnswerDao qaAnswerDao;
 
+    @Autowired
+    private WorkKnowledgeBaseBestAnswerDao bestAnswerDao;
+
     /**
      * 根据文件id加载附件列表(编辑表单回显用)
      */
@@ -751,12 +754,12 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
     }
 
     /**
-     * 问答答疑确认最佳答案后发放积分
+     * 问答答疑确认最佳答案后发放积分(支持多个回答人)
      * @param questionId 问题ID
-     * @param answerUserId 回答人ID
+     * @param answerUserIds 所有回答人ID列表(不重复)
      */
     @Transactional(readOnly = false)
-    public void grantQaPoints(String questionId, String answerUserId) {
+    public void grantQaPointsToMultipleUsers(String questionId, List<String> answerUserIds) {
         // 1. 查询“问答答疑”分类
         List<WorkKnowledgePointCategory> categories = findCategoriesByName("问答答疑");
         if (categories == null || categories.isEmpty()) {
@@ -804,20 +807,25 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
             }
         }
         
-        // 5. 给回答人发放积分(使用固定分值
-        if (StringUtils.isNotBlank(answerUserId)) {
+        // 5. 给每个回答人发放积分(每条最佳答案都发放,同一个人可多次获得
+        if (answerUserIds != null && !answerUserIds.isEmpty()) {
             if (answerRule != null && StringUtils.isNotBlank(answerRule.getPointValue())) {
                 try {
                     Integer answerPoint = Integer.parseInt(answerRule.getPointValue());
                     if (answerPoint > 0) {
-                        insertPointDetail(
-                            answerUserId,                      // 回答人ID
-                            questionId,                        // 问题ID
-                            POINT_CHANGE_AUDIT,               // 变动类型:审核加分(复用)
-                            categoryId,                       // 分类ID
-                            answerRule.getId(),               // 规则ID
-                            answerPoint                       // 积分值
-                        );
+                        // 为每个回答人发放积分(同一个人如果有多条最佳答案,会多次发放)
+                        for (String answerUserId : answerUserIds) {
+                            if (StringUtils.isNotBlank(answerUserId)) {
+                                insertPointDetail(
+                                    answerUserId,                      // 回答人ID
+                                    questionId,                        // 问题ID
+                                    POINT_CHANGE_AUDIT,               // 变动类型:审核加分(复用)
+                                    categoryId,                       // 分类ID
+                                    answerRule.getId(),               // 规则ID
+                                    answerPoint                       // 积分值
+                                );
+                            }
+                        }
                     }
                 } catch (NumberFormatException e) {
                     // 积分值格式错误,跳过
@@ -1130,4 +1138,119 @@ public class WorkKnowledgeBaseShareService extends CrudService<WorkKnowledgeBase
         return QA_STATUS_WAITING.equals(question.getAuditStatus()) 
             || QA_STATUS_ANSWERING.equals(question.getAuditStatus());
     }
+
+    /**
+     * 确认多个最佳答案(最多5条)
+     * @param questionId 问题ID
+     * @param answerIds 答案ID列表
+     * @return 返回所有回答人ID列表(用于发放积分)
+     */
+    @Transactional(readOnly = false)
+    public List<String> confirmMultipleBestAnswers(String questionId, List<String> answerIds) {
+        // 1. 验证参数
+        if (answerIds == null || answerIds.isEmpty()) {
+            throw new RuntimeException("请至少选择一个答案");
+        }
+        if (answerIds.size() > 5) {
+            throw new RuntimeException("最多只能选择5个最佳答案");
+        }
+
+        // 2. 检查问题状态
+        WorkKnowledgeBaseShareInfo question = dao.get(questionId);
+        if (question == null) {
+            throw new RuntimeException("问题不存在");
+        }
+
+        // 3. 检查是否为问答分类
+        boolean isQaCategory = false;
+        if (StringUtils.isNotBlank(question.getTreeNodeId())) {
+            WorkKnowledgeBaseTreeInfo treeInfo = getTreeInfoById(question.getTreeNodeId());
+            if (treeInfo != null && "问答答疑".equals(treeInfo.getTreeName())) {
+                isQaCategory = true;
+            }
+        }
+
+        if (!isQaCategory) {
+            throw new RuntimeException("该分类不支持问答功能");
+        }
+
+        // 4. 检查问题是否已结束
+        if (QA_STATUS_CLOSED.equals(question.getAuditStatus())) {
+            throw new RuntimeException("该问题已被确认答案,无法重复确认");
+        }
+
+        // 5. 验证每个答案是否存在且属于该问题
+        List<WorkKnowledgeBaseQaAnswer> answers = new ArrayList<>();
+        Set<String> answerUserIdSet = new HashSet<>(); // 用于收集不重复的回答人ID
+
+        for (String answerId : answerIds) {
+            WorkKnowledgeBaseQaAnswer answer = qaAnswerDao.get(answerId);
+            if (answer == null || !questionId.equals(answer.getQuestionId())) {
+                throw new RuntimeException("答案[" + answerId + "]不存在或不属于该问题");
+            }
+            answers.add(answer);
+            // 收集回答人ID(自动去重)
+            if (answer.getCreateBy() != null && StringUtils.isNotBlank(answer.getCreateBy().getId())) {
+                answerUserIdSet.add(answer.getCreateBy().getId());
+            }
+        }
+
+        // 6. 删除旧的最佳答案记录(如果存在)
+        bestAnswerDao.deleteByQuestionId(questionId);
+
+        // 7. 批量插入新的最佳答案记录
+        List<WorkKnowledgeBaseBestAnswer> bestAnswers = new ArrayList<>();
+        String currentUserId = UserUtils.getUser().getId();
+        Date now = new Date();
+
+        for (int i = 0; i < answers.size(); i++) {
+            WorkKnowledgeBaseBestAnswer bestAnswer = new WorkKnowledgeBaseBestAnswer();
+            bestAnswer.setIsNewRecord(true);
+            bestAnswer.setId(IdGen.uuid());
+            bestAnswer.setQuestionId(questionId);
+            bestAnswer.setAnswerId(answers.get(i).getId());
+            bestAnswer.setSortOrder(i + 1); // 设置排序号
+            bestAnswer.setCreateBy(new User(currentUserId));
+            bestAnswer.setCreateDate(now);
+            bestAnswers.add(bestAnswer);
+        }
+
+        if (!bestAnswers.isEmpty()) {
+            bestAnswerDao.batchInsert(bestAnswers);
+        }
+
+        // 8. 更新问题状态为已结束(8),记录确认时间
+        question.setAuditStatus(QA_STATUS_CLOSED);
+        question.setConfirmDate(now);
+        question.preUpdate();
+        dao.update(question);
+
+        // 9. 返回所有不重复的回答人ID列表
+        return new ArrayList<>(answerUserIdSet);
+    }
+
+    /**
+     * 查询问题的最佳答案列表
+     * @param questionId 问题ID
+     * @return 最佳答案列表(按排序号排序)
+     */
+    public List<WorkKnowledgeBaseBestAnswer> findBestAnswers(String questionId) {
+        if (StringUtils.isBlank(questionId)) {
+            return new ArrayList<>();
+        }
+        return bestAnswerDao.findByQuestionId(questionId);
+    }
+
+    /**
+     * 检查答案是否已被设为最佳答案
+     * @param questionId 问题ID
+     * @param answerId 答案ID
+     * @return true-已是最佳答案,false-不是
+     */
+    public boolean isBestAnswer(String questionId, String answerId) {
+        if (StringUtils.isBlank(questionId) || StringUtils.isBlank(answerId)) {
+            return false;
+        }
+        return bestAnswerDao.isBestAnswer(questionId, answerId);
+    }
 }

+ 53 - 23
src/main/java/com/jeeplus/modules/WorkKnowledgeBase/web/WorkKnowledgeBaseShareController.java

@@ -305,6 +305,35 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     }
 
     /**
+     * 确认最佳答案专用页面
+     */
+    @RequiresPermissions("workKnowledgeBase:share:list")
+    @RequestMapping(value = "confirmAnswerPage")
+    public String confirmAnswerPage(@RequestParam String id, Model model) {
+        WorkKnowledgeBaseShareInfo shareInfo = shareService.get(id);
+        if (shareInfo == null) {
+            addMessage(model, "记录不存在!");
+            return "error";
+        }
+        
+        // 验证是否为问题发起人
+        String currentUserId = UserUtils.getUser().getId();
+        if (!currentUserId.equals(shareInfo.getCreateBy().getId())) {
+            addMessage(model, "只有问题发起人可以确认答案!");
+            return "error";
+        }
+        
+        // 检查问题是否已结束
+        if ("8".equals(shareInfo.getAuditStatus())) {
+            addMessage(model, "该问题已确认答案,无法重复操作!");
+            return "error";
+        }
+        
+        model.addAttribute("entity", shareInfo);
+        return "modules/WorkKnowledgeBase/workKnowledgeBaseConfirmAnswer";
+    }
+
+    /**
      * 保存文件信息(含动态字段值)
      * 动态字段值以 JSON 数组字符串形式提交:dynamicValuesJson
      * 审核状态根据附件有无自动判定
@@ -604,41 +633,42 @@ public class WorkKnowledgeBaseShareController extends BaseController {
     }
 
     /**
-     * 确认最佳答案(Ajax)
+     * 确认多个最佳答案 - 最多5条
      */
     @RequiresPermissions("workKnowledgeBase:share:list")
-    @RequestMapping(value = "confirmAnswer")
-    @ResponseBody
-    public Map<String, Object> confirmAnswer(@RequestParam String questionId,
-                                             @RequestParam String answerId) {
-        Map<String, Object> result = new HashMap<>();
+    @RequestMapping(value = "confirmMultipleBestAnswers")
+    public String confirmMultipleBestAnswers(@RequestParam String questionId,
+                                             @RequestParam List<String> answerIds,
+                                             HttpServletRequest request, HttpServletResponse response, Model model) {
         try {
             // 验证是否为问题发起人
             String currentUserId = UserUtils.getUser().getId();
             if (!shareService.isQuestionCreator(questionId, currentUserId)) {
-                result.put("success", false);
-                result.put("message", "只有问题发起人可以确认答案");
-                return result;
+                model.addAttribute("message", "只有问题发起人可以确认答案");
+                return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/share/list?message=" + java.net.URLEncoder.encode("只有问题发起人可以确认答案", "UTF-8");
             }
 
-            // Service层会自动处理积分发放逻辑:
-            // 1. 给问题发起人发放积分(rule_type=1,创建积分)
-            // 2. 给回答人发放积分(rule_type=2,回答积分)
-            // 积分规则从“问答答疑”分类对应的配置中读取
-            String answerUserId = shareService.confirmBestAnswer(questionId, answerId);
+            // Service层会:
+            // 1. 保存多条最佳答案到关联表
+            // 2. 给问题发起人发放积分(rule_type=1,创建积分)
+            // 3. 给每个回答人发放积分(rule_type=2,回答积分)
+            //    - 五条内容可以是同一人
+            //    - 每个内容均进行赋分
+            //    - 不限制每个人只能获得1次的分数
+            List<String> answerUserIds = shareService.confirmMultipleBestAnswers(questionId, answerIds);
 
-            // 积分处理:调用Service层方法发放积分
-            shareService.grantQaPoints(questionId, answerUserId);
+            // 积分处理:调用Service层方法发放积分给所有回答人
+            shareService.grantQaPointsToMultipleUsers(questionId, answerUserIds);
 
-
-            result.put("success", true);
-            result.put("message", "答案确认成功,积分已发放");
-            result.put("answerUserId", answerUserId);
+            // 重定向到列表页,并携带成功消息
+            return "redirect:" + Global.getAdminPath() + "/workKnowledgeBase/share/list?message=" + java.net.URLEncoder.encode("答案确认成功,积分已发放", "UTF-8");
         } catch (Exception e) {
-            result.put("success", false);
-            result.put("message", e.getMessage());
+            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";
+            }
         }
-        return result;
     }
 
     /**

+ 71 - 0
src/main/resources/mappings/modules/WorkKnowledgeBase/WorkKnowledgeBaseBestAnswerDao.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeeplus.modules.WorkKnowledgeBase.dao.WorkKnowledgeBaseBestAnswerDao">
+    
+    <sql id="bestAnswerColumns">
+        a.id AS "id",
+        a.question_id AS "questionId",
+        a.answer_id AS "answerId",
+        a.sort_order AS "sortOrder",
+        a.create_by AS "createBy.id",
+        a.create_date AS "createDate",
+        a.del_flag AS "delFlag"
+    </sql>
+    
+    <!-- 根据问题ID查询最佳答案列表 -->
+    <select id="findByQuestionId" resultType="com.jeeplus.modules.WorkKnowledgeBase.entity.WorkKnowledgeBaseBestAnswer">
+        SELECT
+            <include refid="bestAnswerColumns"/>,
+            ans.answer_content AS "answerContent",
+            su.name AS "answerUserName",
+            ans.create_by AS "answerUserId"
+        FROM work_knowledge_base_best_answer a
+        INNER JOIN work_knowledge_base_qa_answer ans ON ans.id = a.answer_id AND ans.del_flag = '0'
+        LEFT JOIN sys_user su ON su.id = ans.create_by
+        WHERE a.question_id = #{questionId}
+          AND a.del_flag = '0'
+        ORDER BY a.sort_order ASC, a.create_date ASC
+    </select>
+    
+    <!-- 统计问题的最佳答案数量 -->
+    <select id="countByQuestionId" resultType="int">
+        SELECT COUNT(1)
+        FROM work_knowledge_base_best_answer
+        WHERE question_id = #{questionId}
+          AND del_flag = '0'
+    </select>
+    
+    <!-- 检查答案是否已被设为最佳答案 -->
+    <select id="isBestAnswer" resultType="boolean">
+        SELECT COUNT(1) > 0
+        FROM work_knowledge_base_best_answer
+        WHERE question_id = #{questionId}
+          AND answer_id = #{answerId}
+          AND del_flag = '0'
+    </select>
+    
+    <!-- 批量插入最佳答案 -->
+    <insert id="batchInsert">
+        INSERT INTO work_knowledge_base_best_answer (
+            id, question_id, answer_id, sort_order, create_by, create_date, del_flag
+        ) VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+                #{item.id},
+                #{item.questionId},
+                #{item.answerId},
+                #{item.sortOrder},
+                #{item.createBy.id},
+                #{item.createDate},
+                '0'
+            )
+        </foreach>
+    </insert>
+    
+    <!-- 删除问题的所有最佳答案 -->
+    <delete id="deleteByQuestionId">
+        DELETE FROM work_knowledge_base_best_answer
+        WHERE question_id = #{questionId}
+    </delete>
+    
+</mapper>

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

@@ -15,7 +15,12 @@
         u.id AS "answerUser.id",  <!-- 回答人ID -->
         u.name AS "answerUserName",
         q.name AS "questionTitle",
-        CASE WHEN q.answer_id = a.id THEN 1 ELSE 0 END AS "isBestAnswer"
+        CASE WHEN EXISTS (
+            SELECT 1 FROM work_knowledge_base_best_answer ba 
+            WHERE ba.question_id = a.question_id 
+              AND ba.answer_id = a.id 
+              AND ba.del_flag = '0'
+        ) THEN 1 ELSE 0 END AS "isBestAnswer"
     </sql>
     
     <sql id="workKnowledgeBaseQaAnswerJoins">

+ 19 - 0
src/main/resources/mysqlDateBase/alter_work_knowledge_base_add_best_answers.sql

@@ -0,0 +1,19 @@
+-- 为问答答疑功能添加最佳答案关联表
+-- 说明: 支持多选最佳答案,最多5条
+
+-- 创建最佳答案关联表
+CREATE TABLE IF NOT EXISTS work_knowledge_base_best_answer (
+    id VARCHAR(64) NOT NULL COMMENT '主键ID',
+    question_id VARCHAR(64) NOT NULL COMMENT '问题ID',
+    answer_id VARCHAR(64) NOT NULL COMMENT '答案ID',
+    sort_order INT DEFAULT 0 COMMENT '排序号',
+    create_by VARCHAR(64) COMMENT '创建人ID',
+    create_date DATETIME COMMENT '创建时间',
+    del_flag CHAR(1) DEFAULT '0' COMMENT '删除标记',
+    PRIMARY KEY (id),
+    KEY idx_question_id (question_id),
+    KEY idx_answer_id (answer_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='问答最佳答案关联表';
+
+-- 注意: 原work_knowledge_base_share_info表中的answer_id字段保留但不再使用
+-- 后续可通过迁移脚本将已有数据迁移到新表中

+ 6 - 0
src/main/resources/mysqlDateBase/alter_work_knowledge_base_share_info_add_question_description.sql

@@ -0,0 +1,6 @@
+-- 为知识库分享信息表添加问题说明字段(富文本)
+-- 说明: 用于存储问答答疑的问题详细说明,支持富文本格式
+
+-- 添加问题说明字段(TEXT类型,支持较长的富文本内容)
+ALTER TABLE work_knowledge_base_share_info 
+ADD COLUMN question_description TEXT DEFAULT NULL COMMENT '问题说明(问答答疑专用,富文本)' AFTER confirm_date;

+ 439 - 0
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseConfirmAnswer.jsp

@@ -0,0 +1,439 @@
+<%@ page contentType="text/html;charset=UTF-8" %>
+<%@ include file="/webpage/include/taglib.jsp" %>
+<html>
+<head>
+    <title>确认最佳答案</title>
+    <%@ include file="/webpage/include/head.jsp" %>
+    <script type="text/javascript" src="${ctxStatic}/layui/layui.js"></script>
+    <link rel='stylesheet' type="text/css" href="${ctxStatic}/layui/css/layui.css"/>
+    <style>
+        .detail-label { font-weight: bold; color: #555; }
+        .detail-value { color: #333; word-break: break-all; }
+        
+        /* 选择按钮样式 */
+        .select-best-btn {
+            padding: 5px 15px;
+            border: 1px solid #d9d9d9;
+            border-radius: 4px;
+            color: #666;
+            cursor: pointer;
+            font-size: 13px;
+            transition: all 0.3s;
+            background: white;
+        }
+        .select-best-btn:hover {
+            color: #ff9800;
+            border-color: #ff9800;
+        }
+        .select-best-btn.selected {
+            background-color: #ff9800;
+            color: white;
+            border-color: #ff9800;
+        }
+        .footer-bar {
+            position: fixed;
+            bottom: 0;
+            left: 0;
+            right: 0;
+            background: white;
+            padding: 15px;
+            box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
+            text-align: center;
+            z-index: 999;
+        }
+        .selected-count {
+            color: #666;
+            margin-right: 20px;
+            font-size: 14px;
+        }
+        .selected-count strong {
+            color: #52c41a;
+            font-size: 18px;
+        }
+    </style>
+</head>
+<body>
+<div class="single-form">
+    <div class="container">
+        <form:form id="inputForm" modelAttribute="entity" action="" method="post" class="layui-form">
+            <input type="hidden" id="questionId" name="questionId" value="${entity.id}"/>
+            
+            <!-- 问答答疑信息 -->
+            <div class="form-group layui-row">
+                <div class="form-group-label"><h2 style="display:inline-block;">问答答疑信息</h2></div>
+                
+                <!-- 问题概述 -->
+                <div class="layui-item layui-col-sm6 lw6">
+                    <label class="layui-form-label detail-label">问题概述:</label>
+                    <div class="layui-input-block">
+                        <input type="text" class="form-control layui-input" value="${entity.name}" readonly="readonly"/>
+                    </div>
+                </div>
+                
+                <!-- 内容属性 -->
+                <div class="layui-item layui-col-sm6 lw6">
+                    <label class="layui-form-label"><span class="require-item">*</span>内容属性:</label>
+                    <div class="layui-input-block">
+                        <input type="radio" name="contentAttribute" disabled lay-filter="contentAttributeRadio" title="原创" value="0">
+                        <input type="radio" name="contentAttribute" disabled lay-filter="contentAttributeRadio" title="转载" value="1">
+                    </div>
+                </div>
+                
+                <!-- 说明(富文本) -->
+                <div class="layui-item layui-col-sm12 lw6" style="padding-bottom: 20px;">
+                    <label class="layui-form-label detail-label">说明:</label>
+                    <div class="layui-input-block">
+                        <div class="wrapForm">
+                            <div class="mask">
+                                <form:textarea path="questionDescription" disabled="true" htmlEscape="false" colspan="3" rows="6" class="form-control"/>
+                                <sys:ckeditorView replace="questionDescription" uploadPath="/workKnowledgeBase/share"/>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 附件展示 -->
+            <c:if test="${not empty entity.workAttachments}">
+                <div class="form-group layui-row">
+                    <div class="form-group-label"><h2>附件信息</h2></div>
+                    <div class="layui-item layui-col-xs12" style="padding:0 16px;">
+                        <table class="table table-bordered table-condensed details">
+                            <thead>
+                            <tr>
+                                <th>文件预览</th>
+                                <th>上传人</th>
+                                <th>上传时间</th>
+                                <th>文件大小(MB)</th>
+                                <th>操作</th>
+                            </tr>
+                            </thead>
+                            <tbody>
+                            <c:forEach items="${entity.workAttachments}" var="attach">
+                                <tr>
+                                    <td>
+                                        <c:choose>
+                                            <c:when test="${fn:containsIgnoreCase(attach.attachmentName,'jpg')
+                                                           or fn:containsIgnoreCase(attach.attachmentName,'png')
+                                                           or fn:containsIgnoreCase(attach.attachmentName,'gif')
+                                                           or fn:containsIgnoreCase(attach.attachmentName,'jpeg')}">
+                                                <img src="${attach.temporaryUrl}" width="50" height="50"
+                                                     onclick="openPreview('${attach.temporaryUrl}',5)"
+                                                     alt="${attach.attachmentName}"/>
+                                            </c:when>
+                                            <c:when test="${fn:containsIgnoreCase(attach.attachmentName,'pdf')}">
+                                                <a class="attention-info" href="javascript:void(0)"
+                                                   onclick="openPreview('${attach.temporaryUrl}',1)">${attach.attachmentName}</a>
+                                            </c:when>
+                                            <c:otherwise>
+                                                <a class="attention-info" href="javascript:void(0)"
+                                                   onclick="openPreview('${attach.temporaryUrl}',2)">${attach.attachmentName}</a>
+                                            </c:otherwise>
+                                        </c:choose>
+                                    </td>
+                                    <td>${attach.createBy.name}</td>
+                                    <td><fmt:formatDate value="${attach.createDate}" type="both"/></td>
+                                    <td>${attach.fileSize}</td>
+                                    <td>
+                                        <a href="${attach.temporaryUrl}" class="op-btn op-btn-download">
+                                            <i class="fa fa-download"></i>&nbsp;下载
+                                        </a>
+                                    </td>
+                                </tr>
+                            </c:forEach>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </c:if>
+            
+            <!-- 发表回答区域 -->
+            <div class="form-group layui-row">
+                <div class="form-group-label"><h2>发表回答</h2></div>
+                <div class="layui-item layui-col-xs12 with-textarea">
+                    <div class="layui-input-block" style="margin-left:0px;position: relative">
+                        <textarea id="answerContent" placeholder="请输入回答内容:" class="form-control required" rows="4" maxlength="2000"></textarea>
+                    </div>
+                    <div style="float: right;margin-right: 10%">
+                        <br/>
+                        <a href="javascript:void(0)" id="submitAnswerBtn" class="layui-btn" onclick="submitAnswer()">发表</a>
+                        <a href="javascript:void(0)" id="resetAnswerBtn" class="layui-btn layui-btn-primary" onclick="resetAnswer()">清空</a>
+                    </div>
+                </div>
+            </div>
+            
+            <!-- 回答列表区域 -->
+            <div class="form-group layui-row">
+                <h2>全部回答(<span id="answerTotalCount">0</span>条)<span class="selected-count">最多可选择5条回复作为最佳答案。当前已选择 <strong id="selectedCount" style="font-weight:normal;">0</strong>/5 条</span></h2>
+                <hr>
+                <div id="answerListContainer"></div>
+            </div>
+            
+            <!-- 底部操作栏 -->
+            <%--<div class="footer-bar">
+
+                <button type="button" class="layui-btn layui-btn-normal" id="submitBtn" onclick="doSubmit()" disabled>
+                    <i class="fa fa-check"></i> 提交确认
+                </button>
+                <button type="button" class="layui-btn layui-btn-primary" onclick="closeWindow()">
+                    <i class="fa fa-close"></i> 关 闭
+                </button>
+            </div>--%>
+        </form:form>
+    </div>
+</div>
+
+    <script src="${ctxStatic}/layer-v2.3/layui/layui.all.js" charset="utf-8"></script>
+    <script>
+        var questionId = '${entity.id}';
+        var currentUserId = '${fns:getUser().id}';
+        var questionCreatorId = '${entity.createBy.id}';
+        var selectedAnswers = [];
+
+        // 页面加载时获取回答列表
+        $(document).ready(function() {
+            loadAnswers();
+        });
+
+        /** 加载回答列表 */
+        function loadAnswers() {
+            $.ajax({
+                type: 'GET',
+                url: '${ctx}/workKnowledgeBase/share/getAnswers',
+                data: { questionId: questionId },
+                success: function(data) {
+                    if (data.success && data.answers) {
+                        renderAnswers(data.answers);
+                        $('#answerTotalCount').text(data.answerCount);
+                    } else {
+                        $('#answerListContainer').html('<div style="padding:40px;text-align:center;color:#999;">暂无回答</div>');
+                    }
+                },
+                error: function() {
+                    $('#answerListContainer').html('<div style="padding:40px;text-align:center;color:red;">加载失败,请刷新重试</div>');
+                }
+            });
+        }
+
+        /** 渲染回答列表 */
+        function renderAnswers(answers) {
+            var html = '';
+            if (answers && answers.length > 0) {
+                for (var i = 0; i < answers.length; i++) {
+                    var ans = answers[i];
+                    html += renderAnswerItem(ans);
+                }
+            } else {
+                html = '<div style="padding:40px;text-align:center;color:#999;">暂无回答</div>';
+            }
+            $('#answerListContainer').html(html);
+        }
+
+        /** 渲染单个回答项(与详情页保持一致) */
+        function renderAnswerItem(ans) {
+            var isCreator = ans.createBy && ans.createBy.id == questionCreatorId;
+            var answerUserId = (ans.answerUser && ans.answerUser.id) || (ans.createBy && ans.createBy.id) || '';
+            var canDelete = currentUserId == answerUserId;
+            
+            var html = '';
+            
+            // 最佳答案使用特殊背景色突出显示
+            if (ans.isBestAnswer == 1) {
+                html += '<div class="layui-item layui-col-xs12" id="answer' + ans.id + '" style="padding:10px 0;background-color:#f6ffed;border-radius:5px;margin-bottom:10px;">';
+            } else {
+                html += '<div class="layui-item layui-col-xs12" id="answer' + ans.id + '" style="padding:10px 0;">';
+            }
+            
+            // 用户名:淡蓝色、字体放大
+            html += '<span class="comment_name" style="color:#1E9FFF;font-size:16px;font-weight:500;">' + (ans.answerUserName || '未知') + '</span>';
+            
+            // 如果是提问者,添加标识
+            if (isCreator) {
+                html += ' <span style="color:#1890ff;font-size:14px;">(提问者)</span>';
+            }
+            
+            // 最佳答案标识
+            if (ans.isBestAnswer == 1) {
+                html += ' <span style="color:#52c41a;font-weight:bold;font-size:14px;">✓ 最佳答案</span>';
+            }
+            
+            // 回答内容:缩进、行高加大
+            html += '<div class="comment_content" style="margin-left:25px;margin-top:8px;font-size:14px;line-height:1.8;color:#333;">' + (ans.answerContent || '') + '</div>';
+            
+            // 底部信息:时间和操作按钮
+            html += '<div class="del" style="color:#999;font-size:12px;margin-top:5px;">';
+            html += '<span>' + (ans.createDate || '') + '</span>';
+            
+            // 删除按钮(仅回答人可看到,最佳答案不能删除)
+            if (ans.isBestAnswer != 1 && canDelete) {
+                html += '<a href="javascript:void(0)" onclick="deleteAnswer(\'' + ans.id + '\')" style="float:right;margin-right:20px;color:#666;font-size:14px;font-weight:500;">删除</a>';
+            }
+            
+            html += '</div>';  // 闭合 .del
+            
+            // 选择按钮(非最佳答案才显示,放在右下方)
+            if (ans.isBestAnswer != 1) {
+                html += '<div style="text-align:right;margin-top:10px;">';
+                html += '<button type="button" class="select-best-btn" data-id="' + ans.id + '" onclick="toggleSelect(this)">选择为最佳答案</button>';
+                html += '</div>';
+            }
+            
+            html += '<hr style="margin:15px 0;border:none;border-top:1px solid #eee;">';
+            html += '</div>';  // 闭合 .layui-item
+            
+            return html;
+        }
+
+        /** 切换选择状态 */
+        function toggleSelect(btn) {
+            var $btn = $(btn);
+            var answerId = $btn.attr('data-id');
+            
+            if ($btn.hasClass('selected')) {
+                // 取消选择
+                $btn.removeClass('selected').text('选择为最佳答案');
+                selectedAnswers = selectedAnswers.filter(function(id) {
+                    return id !== answerId;
+                });
+            } else {
+                // 选择判定
+                if (selectedAnswers.length >= 5) {
+                    layer.msg('最多只能选择5个最佳答案', {icon: 2});
+                    return;
+                }
+                $btn.addClass('selected').text('取消选择');
+                selectedAnswers.push(answerId);
+            }
+            
+            updateUI();
+        }
+
+        /** 更新界面状态 */
+        function updateUI() {
+            $('#selectedCount').text(selectedAnswers.length);
+            
+            if (selectedAnswers.length > 0) {
+                $('#submitBtn').prop('disabled', false).removeClass('layui-btn-disabled');
+            } else {
+                $('#submitBtn').prop('disabled', true).addClass('layui-btn-disabled');
+            }
+        }
+
+        /** 提交回答 */
+        function submitAnswer() {
+            var content = $('#answerContent').val().trim();
+            if (!content) {
+                layer.msg('请输入回答内容', {icon: 2});
+                return;
+            }
+            
+            $.ajax({
+                type: 'POST',
+                url: '${ctx}/workKnowledgeBase/share/submitAnswer',
+                data: {
+                    questionId: questionId,
+                    answerContent: content
+                },
+                success: function(data) {
+                    if (data.success) {
+                        layer.msg('回答提交成功', {icon: 1});
+                        $('#answerContent').val('');
+                        loadAnswers(); // 刷新列表
+                    } else {
+                        layer.msg(data.message || '回答失败', {icon: 2});
+                    }
+                },
+                error: function() {
+                    layer.msg('请求失败,请重试', {icon: 2});
+                }
+            });
+        }
+
+        /** 清空回答 */
+        function resetAnswer() {
+            $('#answerContent').val('');
+        }
+
+        /** 删除回答 */
+        function deleteAnswer(answerId) {
+            layer.confirm('确认删除此回答吗?', {
+                icon: 3,
+                title: '删除提示'
+            }, function(index){
+                $.ajax({
+                    type: 'POST',
+                    url: '${ctx}/workKnowledgeBase/share/deleteAnswer',
+                    data: {
+                        answerId: answerId
+                    },
+                    success: function(data) {
+                        if (data.success) {
+                            layer.msg('删除成功', {icon: 1});
+                            layer.close(index);
+                            loadAnswers(); // 刷新列表
+                        } else {
+                            layer.msg(data.message || '删除失败', {icon: 2});
+                        }
+                    },
+                    error: function() {
+                        layer.msg('请求失败,请重试', {icon: 2});
+                    }
+                });
+            });
+        }
+
+        /** 提交确认最佳答案 */
+        function doSubmit() {
+            // 验证至少选择1条
+            if (selectedAnswers.length === 0) {
+                layer.msg('请至少选择一个答案', {icon: 2});
+                return false;
+            }
+
+            // 创建隐藏字段并添加到表单中
+            var form = $('#inputForm');
+            
+            // 移除旧的answerIds字段(如果存在)
+            form.find('input[name="answerIds"]').remove();
+
+            
+            // 为每个选中的答案ID创建隐藏字段
+            for (var i = 0; i < selectedAnswers.length; i++) {
+                form.append('<input type="hidden" name="answerIds" value="' + selectedAnswers[i] + '"/>');
+            }
+            
+            // 设置表单action和method
+            form.attr('action', '${ctx}/workKnowledgeBase/share/confirmMultipleBestAnswers');
+            form.attr('method', 'POST');
+            
+            // 设置 target 为 _self,让 redirect 结果只在弹窗 iframe 内加载,不影响父页面
+            form.attr('target', '_self');
+            form.submit();
+            return true;
+        }
+
+        /** 暴露给父窗口调用 */
+        window.doSubmit = doSubmit;
+
+        /** 关闭窗口 */
+        function closeWindow() {
+            var index = parent.layer.getFrameIndex(window.name);
+            parent.layer.close(index);
+        }
+
+        /** 打开预览 */
+        function openPreview(url, type) {
+            if (type == 1) { // PDF
+                window.open(url);
+            } else if (type == 2) { // Word/Excel
+                window.open(url);
+            } else if (type == 5) { // 图片
+                top.layer.photos({
+                    photos: {data: [{src: url}]},
+                    anim: 5
+                });
+            }
+        }
+    </script>
+</body>
+</html>

+ 15 - 22
src/main/webapp/webpage/modules/WorkKnowledgeBase/workKnowledgeBaseShareDetail.jsp

@@ -580,25 +580,25 @@
         console.log('开始渲染回答,数量:', answers ? answers.length : 0); // 调试信息
         var html = '';
         if (answers && answers.length > 0) {
-            // 分离最佳答案和其他答案
-            var bestAnswer = null;
+            // 分离最佳答案和其他答案(支持多个最佳答案)
+            var bestAnswers = [];  // 改为数组,支持多个最佳答案
             var otherAnswers = [];
             
             for (var i = 0; i < answers.length; i++) {
                 var ans = answers[i];
                 if (ans.isBestAnswer == 1) {
-                    bestAnswer = ans;
+                    bestAnswers.push(ans);  // 添加到最佳答案数组
                 } else {
                     otherAnswers.push(ans);
                 }
             }
             
             // 判断是否已有最佳答案
-            var hasBestAnswer = (bestAnswer != null);
+            var hasBestAnswer = (bestAnswers.length > 0);
             
-            // 如果有最佳答案,先渲染最佳答案(置顶)
-            if (bestAnswer) {
-                html += renderAnswerItem(bestAnswer, true, hasBestAnswer);
+            // 如果有最佳答案,先渲染所有最佳答案(置顶)
+            for (var i = 0; i < bestAnswers.length; i++) {
+                html += renderAnswerItem(bestAnswers[i], true, hasBestAnswer);
             }
             
             // 渲染其他答案
@@ -618,16 +618,7 @@
         // 获取回答人ID:优先使用answerUser.id(从数据库查询),其次使用createBy.id
         var answerUserId = (ans.answerUser && ans.answerUser.id) || (ans.createBy && ans.createBy.id) || '';
         var canDelete = currentUserId == answerUserId; // 只有回答人可以删除自己的回答
-        
-        // 调试日志:打印回答信息
-        console.log('回答ID:', ans.id);
-        console.log('  - answerUser:', ans.answerUser);
-        console.log('  - createBy:', ans.createBy);
-        console.log('  - answerUserId:', answerUserId);
-        console.log('  - currentUserId:', currentUserId);
-        console.log('  - canDelete:', canDelete);
-        console.log('  - isBest:', isBest);
-        console.log('  - hasBestAnswer:', hasBestAnswer);
+
         
         var html = '';
         
@@ -664,7 +655,8 @@
         }
         
         // 确认按钮(仅发起人且未确认答案时显示,只读模式不显示)
-        if (!isBest && currentUserId == questionCreatorId && !hasBestAnswer && !readOnly) {
+        // 注意:多选模式下,只要该回答还不是最佳答案,就可以选择
+        if (!isBest && currentUserId == questionCreatorId && !readOnly) {
             html += '<a href="javascript:void(0)" onclick="confirmAnswer(\'' + ans.id + '\')" style="float:right;margin-right:10px;color:#52c41a;font-size:14px;font-weight:500;">确认为最佳答案</a>';
         }
         
@@ -710,19 +702,20 @@
         $('#answerContent').val('');
     }
 
-    /** 确认最佳答案 */
+    /** 确认最佳答案(快捷操作,将单个答案添加为最佳答案) */
     function confirmAnswer(answerId) {
-        layer.confirm('确认选择此答案为最佳答案吗?确认后问题将关闭,其他人无法继续回答。', {
+        layer.confirm('确认将此回答设为最佳答案吗?', {
             icon: 3,
             title: '确认提示'
         }, function(index){
             $.ajax({
                 type: 'POST',
-                url: '${ctx}/workKnowledgeBase/share/confirmAnswer',
+                url: '${ctx}/workKnowledgeBase/share/confirmMultipleBestAnswers',
                 data: {
                     questionId: questionId,
-                    answerId: answerId
+                    answerIds: [answerId]  // 使用数组格式,与多选接口保持一致
                 },
+                traditional: true,  // 允许传递数组
                 success: function(data) {
                     if (data.success) {
                         layer.msg('答案确认成功,积分已发放', {icon: 1});

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

@@ -10,6 +10,15 @@
         var currentRootId = '${rootId}';
 
         $(document).ready(function() {
+            // 检查URL参数中的message并显示提示
+            var urlParams = new URLSearchParams(window.location.search);
+            var message = urlParams.get('message');
+            if (message) {
+                layer.msg(decodeURIComponent(message), {icon: 1});
+                // 清除URL中的message参数,避免重复显示
+                window.history.replaceState({}, document.title, window.location.pathname + window.location.search.replace(/[?&]message=[^&]+/, ''));
+            }
+            
             // moresee 展开/收起动态字段查询区
             $('#moresee').click(function(){
                 if($('#moresees').is(':visible')) {
@@ -142,6 +151,42 @@
             });
         }
         
+        /** 打开确认最佳答案页面(带提交/关闭按钮) */
+        function openConfirmAnswerDialog(id) {
+            if (navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i)) {
+                var width = 'auto'; 
+                var height = 'auto';
+            } else {
+                var width = '80%'; 
+                var height = '80%';
+            }
+            top.layer.open({
+                type: 2,
+                area: [width, height],
+                title: '确认最佳答案',
+                maxmin: true,
+                content: '${ctx}/workKnowledgeBase/share/confirmAnswerPage?id=' + id,
+                skin: 'three-btns',
+                btn: ['提交', '关闭'],
+                btn1: function(index, layero) {
+                    var body = top.layer.getChildFrame('body', index);
+                    var iframeWin = layero.find('iframe')[0];
+                    var inputForm = body.find('#inputForm');
+                    inputForm.attr('target', '_self');
+                    if (iframeWin.contentWindow.doSubmit(1)) {
+                        setTimeout(function() {
+                            top.layer.close(index);
+                            // 弹窗关闭后刷新右侧列表
+                            search();
+                        }, 300);
+                    }
+                },
+                btn2: function(index) { 
+                    top.layer.close(index); 
+                }
+            });
+        }
+        
         /** 预览文件(带阅读记录) */
         function openPreviewWithRead(url, type, fileId) {
             // 先记录阅读行为
@@ -694,7 +739,7 @@
                     if (isCreator) {
                         // 待回答(6)或回答中(7)状态:显示撤回和确认答案按钮
                         if (qaStatus === 7) {
-                            xml += '<a href="javascript:void(0)" onclick="openDetailDialogWithRead(\'' + d.id + '\')" class="layui-btn layui-btn-xs layui-bg-green"> 确认答案</a>';
+                            xml += '<a href="javascript:void(0)" onclick="openConfirmAnswerDialog(\'' + d.id + '\')" class="layui-btn layui-btn-xs layui-bg-green"> 确认答案</a>';
                         }
                         if (qaStatus === 6 || qaStatus === 7) {
                             xml += '<a href="javascript:void(0)" onclick="withdrawAudit(\'' + d.id + '\')" class="layui-btn layui-btn-xs layui-bg-orange"> 撤回</a>';