diff --git a/schoolNewsServ/.bin/dify/部署注意点.md b/schoolNewsServ/.bin/dify/部署注意点.md
new file mode 100644
index 0000000..9c2ac1b
--- /dev/null
+++ b/schoolNewsServ/.bin/dify/部署注意点.md
@@ -0,0 +1,9 @@
+# 1.工作流修改
+## 动态知识库检索工作流
+修改dataset的apikey
+
+## 思政小帮手对话流
+修改“动态知识库检索”的入参
+
+# 2. 后端修改
+修改yaml的配置内容
\ No newline at end of file
diff --git a/schoolNewsServ/admin/src/main/resources/application.yml b/schoolNewsServ/admin/src/main/resources/application.yml
index e2d73dd..eaa1118 100644
--- a/schoolNewsServ/admin/src/main/resources/application.yml
+++ b/schoolNewsServ/admin/src/main/resources/application.yml
@@ -111,6 +111,7 @@ school-news:
- "/public/**"
- "/static/**"
- "/file/download/**"
+ - "/ai/chat/stream/**"
crawler:
@@ -158,6 +159,10 @@ crontab:
type: Boolean
value: true
+
+# dify
+dify:
+ knowledgeApiKey: dataset-nupqKP4LONpzdXmGthIrbjeJ
# 文件存储配置
file:
storage:
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java
index 6dfb20a..72060c0 100644
--- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java
@@ -514,6 +514,13 @@ public class DifyApiClient {
}
}
}
+ // 流正常读取完毕,如果没有收到 [DONE] 标记,也当作完成
+ logger.info("SSE流读取完毕");
+ callback.onComplete();
+ } catch (java.io.EOFException e) {
+ // EOFException 通常表示流正常结束(Dify可能没有发送[DONE]标记)
+ logger.info("SSE流已结束(EOF)");
+ callback.onComplete();
} catch (Exception e) {
logger.error("流式响应处理异常", e);
callback.onError(e);
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java
index b66520e..2d4a826 100644
--- a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java
@@ -31,9 +31,9 @@ public class DifyConfig {
* Dify API密钥(默认密钥,可被智能体的密钥覆盖)
*/
// private String apiKey="app-PTHzp2DsLPyiUrDYTXBGxL1f";
- private String apiKey="app-fwOqGFLTsZtekCQYlOmj9f8x";
+ private String apiKey="app-6VXXDHRC8fiTijeRfnwzOyGn";
- private String knowledgeApiKey="dataset-HeDK9gHBqPnI4rBZ2q2Hm7rV";
+ private String knowledgeApiKey="dataset-nupqKP4LONpzdXmGthIrbjeJ";
/**
* 请求超时时间(秒)
diff --git a/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml b/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml
index 732b510..4039bb9 100644
--- a/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml
+++ b/schoolNewsServ/study/src/main/resources/mapper/LearningRecordMapper.xml
@@ -148,7 +148,7 @@
complete_time = #{completeTime},
- last_learn_time = #{lastLearnTime},
+ last_learn_time = #{updateTime},
updater = #{updater},
@@ -298,8 +298,8 @@
) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%')
WHERE lr.deleted = 0
- AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
- AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
+ AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
+ AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
GROUP BY lr.resource_type
ORDER BY totalDuration DESC
@@ -326,8 +326,8 @@
) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%')
WHERE lr.deleted = 0
- AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
- AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
+ AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
+ AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
GROUP BY lr.user_id, u.username
ORDER BY totalDuration DESC
LIMIT 10
@@ -357,8 +357,8 @@
WHERE lr.deleted = 0
AND lr.resource_type = 2
- AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
- AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
+ AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
+ AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
GROUP BY lr.resource_id, c.name
ORDER BY learnerCount DESC, totalDuration DESC
LIMIT 10
@@ -388,8 +388,8 @@
WHERE lr.deleted = 0
AND lr.resource_type = 1
- AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
- AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
+ AND lr.update_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
+ AND lr.update_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
GROUP BY lr.resource_id, r.title
ORDER BY learnerCount DESC, totalDuration DESC
LIMIT 10
@@ -419,7 +419,7 @@
LEFT JOIN tb_learning_record lr ON tu.user_id = lr.user_id AND tu.task_id = lr.task_id AND lr.deleted = 0
WHERE tu.deleted = 0
AND tu.status = 2
- AND tu.complete_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
+ AND tu.complete_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
AND tu.complete_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
GROUP BY tu.user_id, u.username
ORDER BY completedTaskCount DESC, totalDuration DESC
diff --git a/schoolNewsWeb/src/apis/ai/chat.ts b/schoolNewsWeb/src/apis/ai/chat.ts
index d1d2d7a..28090fb 100644
--- a/schoolNewsWeb/src/apis/ai/chat.ts
+++ b/schoolNewsWeb/src/apis/ai/chat.ts
@@ -127,7 +127,35 @@ export const chatApi = {
// 监听错误事件
eventSource.addEventListener('error', (event: any) => {
- const error = new Error(event.data || '对话失败');
+ // 如果 data 为空,说明是正常的流结束信号(有些工作流不发送end事件)
+ if (!event.data || event.data.trim() === '') {
+ console.log('[SSE流结束] 收到空error事件,当作正常结束处理');
+
+ // 触发结束回调(如果有的话)
+ callback?.onMessageEnd?.({
+ conversationId: request.conversationId,
+ messageId: '',
+ answer: fullMessage
+ } as any);
+
+ eventSource.close();
+
+ // 正常结束
+ resolve({
+ code: 200,
+ success: true,
+ login: true,
+ auth: true,
+ data: {
+ conversationId: request.conversationId,
+ answer: fullMessage
+ } as any,
+ message: '对话已结束'
+ });
+ return;
+ }
+
+ const error = new Error(event.data);
callback?.onError?.(error);
eventSource.close();
reject(error);
@@ -358,7 +386,34 @@ export const chatApi = {
// 监听错误事件
eventSource.addEventListener('error', (event: any) => {
- const error = new Error(event.data || '重新生成失败');
+ // 如果 data 为空,说明是正常的流结束信号(有些工作流不发送end事件)
+ if (!event.data || event.data.trim() === '') {
+ console.log('[SSE流结束-重新生成] 收到空error事件,当作正常结束处理');
+
+ // 触发结束回调(如果有的话)
+ callback?.onMessageEnd?.({
+ messageId: messageId,
+ answer: fullMessage
+ } as any);
+
+ eventSource.close();
+
+ // 正常结束
+ resolve({
+ code: 200,
+ success: true,
+ login: true,
+ auth: true,
+ data: {
+ messageId: messageId,
+ answer: fullMessage
+ } as any,
+ message: '重新生成已结束'
+ });
+ return;
+ }
+
+ const error = new Error(event.data);
callback?.onError?.(error);
eventSource.close();
reject(error);
diff --git a/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue b/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue
index 4b45a3c..9d6bde8 100644
--- a/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue
+++ b/schoolNewsWeb/src/views/admin/manage/study/StudyRecordsView.vue
@@ -180,7 +180,14 @@ function initDurationChart(data: any[]) {
const duration = params.value;
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
- return `${params.name}
学习时长: ${hours}小时${minutes}分钟
学习人数: ${params.data.userCount}人`;
+ const secs = duration % 60;
+
+ let timeStr = '';
+ if (hours > 0) timeStr += `${hours}小时`;
+ if (minutes > 0) timeStr += `${minutes}分钟`;
+ if (secs > 0 || timeStr === '') timeStr += `${secs}秒`;
+
+ return `${params.name}
学习时长: ${timeStr}
学习人数: ${params.data.userCount}人`;
}
},
legend: {
@@ -204,7 +211,18 @@ function initDurationChart(data: any[]) {
const duration = params.value;
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
- return `${params.name}\n${hours}h${minutes}m`;
+ const secs = duration % 60;
+
+ let timeStr = '';
+ if (hours > 0) {
+ timeStr = `${hours}h${minutes}m`;
+ } else if (minutes > 0) {
+ timeStr = `${minutes}m${secs}s`;
+ } else {
+ timeStr = `${secs}s`;
+ }
+
+ return `${params.name}\n${timeStr}`;
}
},
emphasis: {
@@ -233,12 +251,26 @@ function initDurationChart(data: any[]) {
// 格式化时长
function formatDuration(seconds: number): string {
+ if (!seconds || seconds === 0) {
+ return '0秒';
+ }
+
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = seconds % 60;
+
+ let result = '';
if (hours > 0) {
- return `${hours}小时${minutes}分钟`;
+ result += `${hours}小时`;
}
- return `${minutes}分钟`;
+ if (minutes > 0) {
+ result += `${minutes}分钟`;
+ }
+ if (secs > 0 || result === '') {
+ result += `${secs}秒`;
+ }
+
+ return result;
}
// 获取排名样式
@@ -294,7 +326,7 @@ function getRankClass(index: number): string {
&:hover {
background: #e8edf3;
- transform: translateX(5px);
+ // transform: translateX(5px);
}
.rank {