diff --git a/.claude/worktrees/hopeful-goldberg b/.claude/worktrees/hopeful-goldberg deleted file mode 160000 index c69ce28..0000000 --- a/.claude/worktrees/hopeful-goldberg +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c69ce2891f5d58cf7fb97c2bf6bc504453280aee diff --git a/.claude/worktrees/suspicious-antonelli b/.claude/worktrees/suspicious-antonelli deleted file mode 160000 index 6f2dc27..0000000 --- a/.claude/worktrees/suspicious-antonelli +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6f2dc27fbc4d3a2755e5322ae86e47ad343eef8d diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e363fbe --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.claude/worktrees/ diff --git a/README.md b/README.md index 469abdb..f9b57f6 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,15 @@ msh-system/ --- +## 运行环境 + +- 云服务器mysql数据库, 数据库名: shop-msh + IP&端口:49.235.131.69:3306 + username: root + password: mogu2018 + +--- + ## 技术栈概览 | 层次 | 技术项 | 说明 | @@ -172,6 +181,11 @@ mvn clean package --- +## 参考网站 +1. https://www.ishen365.com/sfsn + +--- + ## 文档索引 | 文档 | 路径 | diff --git a/docs/Testing/test-0325-1.md b/docs/Testing/test-0325-1.md index 8acfe9b..89f261a 100644 --- a/docs/Testing/test-0325-1.md +++ b/docs/Testing/test-0325-1.md @@ -1,23 +1,42 @@ # 手动测试问题 -## 页面(pages/tool/food-encyclopedia)测试 -- 1. **已修复**页面(pages/tool/food-encyclopedia)报错: -- 2. **已修复**页面(pages/tool/food-encyclopedia)中class="category-badge"改成显示中文 -- 3. **已修复**页面(pages/tool/food-encyclopedia)点击进入详情页 - -## 页面(pages/tool/nutrient-detail?name=%E9%92%BE) - -- 1. **已修复**显示空白页,返回数据为空,如果是因为v2_knowledge 表尚无营养素数据,通过ai生成需要的数据可以插入到v2_knowledge表中 - - ## 页面(pages/tool/ai-nutritionist) -- 1. 优化方案:/Users/a123/msh-system/docs/功能开发详细设计_2026-03-25.md -- 2. 对话响应还是很慢,是否可以使用SSE流式对话来优化响应速度? -- 3. **已修复** 会话错误:"发起对话失败:未返回会话或对话ID" +- 1. 请求后页面显示:"未能获取到有效回复。" +fetch("http://127.0.0.1:20822/api/front/coze/chat/stream", { + "headers": { + "accept": "*/*", + "authori-zation": "6f6767b2edc64949b0e4888c199ac0bb", + "content-type": "application/json", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site" + }, + "referrer": "https://servicewechat.com/wx7ecf3e3699353c69/devtools/page-frame.html", + "referrerPolicy": "strict-origin-when-cross-origin", + "body": "{\"botId\":\"7591133240535449654\",\"userId\":11,\"additionalMessages\":[{\"role\":\"user\",\"content\":\"透析患者可以喝牛奶吗?\",\"content_type\":\"text\"}],\"stream\":true,\"autoSaveHistory\":true}", + "method": "POST", + "mode": "cors", + "credentials": "omit" +}); +## 修复记录 + +### 问题 1 修复:流式对话显示"未能获取到有效回复" + +**根因分析**:两个问题导致前端无法正确接收流式数据: + +1. **Delta 事件过滤条件过严** — `ai-nutritionist.vue` 中 `sendToAIStream()` 对 `conversation.message.delta` 事件要求 `evt.role === 'assistant' && evt.type === 'answer'`。但 Coze SDK 在流式增量事件中可能不返回 `role` 和 `type` 字段(后端发送的精简 JSON 仅在字段非 null 时才包含),导致所有增量内容被静默丢弃。 + +2. **未处理非分块响应降级** — `cozeChatStream()` 中 `success` 回调未处理响应体。在微信开发者工具或某些不支持 `onChunkReceived` 的环境下,流式数据仅在 `res.data` 中一次性返回,但被完全忽略。 + +**修复内容**: +- `ai-nutritionist.vue`:将 delta 过滤改为 `const role = evt.role || 'assistant'`,缺失字段时默认为预期值。 +- `models-api.js`:增加 `_gotChunks` 标记,当 `onChunkReceived` 未触发时,在 `success` 回调中解析 `res.data` 作为降级处理;增加 `responseType: 'text'` 确保响应体为字符串。 + # 参考文档 +- 3. /Users/a123/msh-system/.cursor/plans/optimize_ai_nutritionist_speed_b6e9a618.plan.md - 1. /Users/a123/msh-system/docs/测试问题分析报告_2026-03-22.md - 2. /Users/a123/msh-system/docs/功能开发详细设计_2026-03-25.md \ No newline at end of file diff --git a/docs/db/shop-msh.sql b/docs/db/shop-msh.sql index 10d821c..62ea225 100644 --- a/docs/db/shop-msh.sql +++ b/docs/db/shop-msh.sql @@ -11,7 +11,7 @@ Target Server Version : 80022 (8.0.22) File Encoding : 65001 - Date: 01/02/2026 22:29:26 + Date: 25/03/2026 11:49:10 */ SET NAMES utf8mb4; @@ -158,7 +158,7 @@ CREATE TABLE `eb_article` ( KEY `idx_post_id` (`post_id`), KEY `idx_check_in_record_id` (`check_in_record_id`), KEY `idx_type_status_task` (`type`,`status_task`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='文章管理表'; +) ENGINE=InnoDB AUTO_INCREMENT=42 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='文章管理表'; -- ---------------------------- -- Table structure for eb_category @@ -195,7 +195,7 @@ CREATE TABLE `eb_exception_log` ( `exp_detail` longtext COMMENT '异常详细信息', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=109 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='异常信息表'; +) ENGINE=InnoDB AUTO_INCREMENT=2581 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='异常信息表'; -- ---------------------------- -- Table structure for eb_express @@ -577,7 +577,7 @@ CREATE TABLE `eb_product_day_record` ( `order_success_product_fee` decimal(8,2) DEFAULT NULL COMMENT '销售额', PRIMARY KEY (`id`) USING BTREE, KEY `date` (`date`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=14056 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='商品日记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=14119 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='商品日记录表'; -- ---------------------------- -- Table structure for eb_schedule_job @@ -611,7 +611,7 @@ CREATE TABLE `eb_schedule_job_log` ( `times` int NOT NULL COMMENT '耗时(单位:毫秒)', `create_time` datetime DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`log_id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=4624947 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='定时任务日志'; +) ENGINE=InnoDB AUTO_INCREMENT=4625316 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='定时任务日志'; -- ---------------------------- -- Table structure for eb_sensitive_method_log @@ -705,7 +705,7 @@ CREATE TABLE `eb_shopping_product_day_record` ( `order_success_product_num` int DEFAULT NULL COMMENT '交易成功商品数', PRIMARY KEY (`id`) USING BTREE, KEY `date` (`date`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=271 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='商城商品日记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=272 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='商城商品日记录表'; -- ---------------------------- -- Table structure for eb_sms_record @@ -1448,7 +1448,7 @@ CREATE TABLE `eb_system_attachment` ( `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`att_id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=1494 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='附件管理表'; +) ENGINE=InnoDB AUTO_INCREMENT=1581 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='附件管理表'; -- ---------------------------- -- Table structure for eb_system_city @@ -1693,7 +1693,7 @@ CREATE TABLE `eb_trading_day_record` ( `brokerage_fee` decimal(8,2) DEFAULT NULL COMMENT '支付佣金金额(用户确认到账佣金)', PRIMARY KEY (`id`) USING BTREE, KEY `date` (`date`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=271 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='商城交易日记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=272 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='商城交易日记录表'; -- ---------------------------- -- Table structure for eb_user @@ -1747,7 +1747,7 @@ CREATE TABLE `eb_user` ( KEY `level` (`level`) USING BTREE, KEY `status` (`status`) USING BTREE, KEY `is_promoter` (`is_promoter`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户表'; +) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户表'; -- ---------------------------- -- Table structure for eb_user_address @@ -1853,7 +1853,7 @@ CREATE TABLE `eb_user_experience_record` ( KEY `add_time` (`create_time`) USING BTREE, KEY `type` (`type`) USING BTREE, KEY `type_link` (`type`,`link_id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户经验记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户经验记录表'; -- ---------------------------- -- Table structure for eb_user_extract @@ -1921,7 +1921,7 @@ CREATE TABLE `eb_user_integral_record` ( KEY `type` (`type`) USING BTREE, KEY `type_link` (`type`,`link_id`) USING BTREE, KEY `idx_source_detail` (`source_detail`) -) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户积分记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户积分记录表'; -- ---------------------------- -- Table structure for eb_user_level @@ -1942,7 +1942,7 @@ CREATE TABLE `eb_user_level` ( `expired_time` timestamp NULL DEFAULT NULL COMMENT '过期时间', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `id` (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户等级记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户等级记录表'; -- ---------------------------- -- Table structure for eb_user_recharge @@ -2020,7 +2020,7 @@ CREATE TABLE `eb_user_sign` ( KEY `idx_report_id` (`report_id`), KEY `idx_nutrition_score` (`nutrition_score`), KEY `idx_copied_from` (`copied_from_sign_id`) -) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='签到记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='签到记录表'; -- ---------------------------- -- Table structure for eb_user_tag @@ -2046,7 +2046,7 @@ CREATE TABLE `eb_user_token` ( `login_ip` varchar(32) DEFAULT NULL COMMENT '登录ip', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `type+token` (`type`,`token`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; +) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; -- ---------------------------- -- Table structure for eb_user_visit_record @@ -2059,7 +2059,7 @@ CREATE TABLE `eb_user_visit_record` ( `visit_type` int DEFAULT NULL COMMENT '访问类型', PRIMARY KEY (`id`) USING BTREE, KEY `date` (`date`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=1470 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户访问记录表'; +) ENGINE=InnoDB AUTO_INCREMENT=1807 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户访问记录表'; -- ---------------------------- -- Table structure for eb_wechat_callback @@ -2090,7 +2090,7 @@ CREATE TABLE `eb_wechat_exceptions` ( `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE=InnoDB AUTO_INCREMENT=1311456 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='微信异常表'; +) ENGINE=InnoDB AUTO_INCREMENT=1311767 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='微信异常表'; -- ---------------------------- -- Table structure for eb_wechat_pay_info @@ -2186,6 +2186,179 @@ CREATE TABLE `eb_wechat_reply` ( KEY `status` (`status`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='微信关键字回复表'; +-- ---------------------------- +-- Table structure for qrtz_blob_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_blob_triggers`; +CREATE TABLE `qrtz_blob_triggers` ( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_NAME` varchar(200) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + `BLOB_DATA` blob, + PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `QRTZ_BLOB_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_calendars +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_calendars`; +CREATE TABLE `qrtz_calendars` ( + `SCHED_NAME` varchar(120) NOT NULL, + `CALENDAR_NAME` varchar(200) NOT NULL, + `CALENDAR` blob NOT NULL, + PRIMARY KEY (`SCHED_NAME`,`CALENDAR_NAME`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_cron_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_cron_triggers`; +CREATE TABLE `qrtz_cron_triggers` ( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_NAME` varchar(200) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + `CRON_EXPRESSION` varchar(200) NOT NULL, + `TIME_ZONE_ID` varchar(80) DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `QRTZ_CRON_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_fired_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_fired_triggers`; +CREATE TABLE `qrtz_fired_triggers` ( + `SCHED_NAME` varchar(120) NOT NULL, + `ENTRY_ID` varchar(95) NOT NULL, + `TRIGGER_NAME` varchar(200) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + `INSTANCE_NAME` varchar(200) NOT NULL, + `FIRED_TIME` bigint NOT NULL, + `SCHED_TIME` bigint NOT NULL, + `PRIORITY` int NOT NULL, + `STATE` varchar(16) NOT NULL, + `JOB_NAME` varchar(200) DEFAULT NULL, + `JOB_GROUP` varchar(200) DEFAULT NULL, + `IS_NONCONCURRENT` varchar(1) DEFAULT NULL, + `REQUESTS_RECOVERY` varchar(1) DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`,`ENTRY_ID`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_job_details +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_job_details`; +CREATE TABLE `qrtz_job_details` ( + `SCHED_NAME` varchar(120) NOT NULL, + `JOB_NAME` varchar(200) NOT NULL, + `JOB_GROUP` varchar(200) NOT NULL, + `DESCRIPTION` varchar(250) DEFAULT NULL, + `JOB_CLASS_NAME` varchar(250) NOT NULL, + `IS_DURABLE` varchar(1) NOT NULL, + `IS_NONCONCURRENT` varchar(1) NOT NULL, + `IS_UPDATE_DATA` varchar(1) NOT NULL, + `REQUESTS_RECOVERY` varchar(1) NOT NULL, + `JOB_DATA` blob, + PRIMARY KEY (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_locks +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_locks`; +CREATE TABLE `qrtz_locks` ( + `SCHED_NAME` varchar(120) NOT NULL, + `LOCK_NAME` varchar(40) NOT NULL, + PRIMARY KEY (`SCHED_NAME`,`LOCK_NAME`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_paused_trigger_grps +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_paused_trigger_grps`; +CREATE TABLE `qrtz_paused_trigger_grps` ( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + PRIMARY KEY (`SCHED_NAME`,`TRIGGER_GROUP`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_scheduler_state +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_scheduler_state`; +CREATE TABLE `qrtz_scheduler_state` ( + `SCHED_NAME` varchar(120) NOT NULL, + `INSTANCE_NAME` varchar(200) NOT NULL, + `LAST_CHECKIN_TIME` bigint NOT NULL, + `CHECKIN_INTERVAL` bigint NOT NULL, + PRIMARY KEY (`SCHED_NAME`,`INSTANCE_NAME`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_simple_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_simple_triggers`; +CREATE TABLE `qrtz_simple_triggers` ( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_NAME` varchar(200) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + `REPEAT_COUNT` bigint NOT NULL, + `REPEAT_INTERVAL` bigint NOT NULL, + `TIMES_TRIGGERED` bigint NOT NULL, + PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `QRTZ_SIMPLE_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_simprop_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_simprop_triggers`; +CREATE TABLE `qrtz_simprop_triggers` ( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_NAME` varchar(200) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + `STR_PROP_1` varchar(512) DEFAULT NULL, + `STR_PROP_2` varchar(512) DEFAULT NULL, + `STR_PROP_3` varchar(512) DEFAULT NULL, + `INT_PROP_1` int DEFAULT NULL, + `INT_PROP_2` int DEFAULT NULL, + `LONG_PROP_1` bigint DEFAULT NULL, + `LONG_PROP_2` bigint DEFAULT NULL, + `DEC_PROP_1` decimal(13,4) DEFAULT NULL, + `DEC_PROP_2` decimal(13,4) DEFAULT NULL, + `BOOL_PROP_1` varchar(1) DEFAULT NULL, + `BOOL_PROP_2` varchar(1) DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `QRTZ_SIMPROP_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `qrtz_triggers` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + +-- ---------------------------- +-- Table structure for qrtz_triggers +-- ---------------------------- +DROP TABLE IF EXISTS `qrtz_triggers`; +CREATE TABLE `qrtz_triggers` ( + `SCHED_NAME` varchar(120) NOT NULL, + `TRIGGER_NAME` varchar(200) NOT NULL, + `TRIGGER_GROUP` varchar(200) NOT NULL, + `JOB_NAME` varchar(200) NOT NULL, + `JOB_GROUP` varchar(200) NOT NULL, + `DESCRIPTION` varchar(250) DEFAULT NULL, + `NEXT_FIRE_TIME` bigint DEFAULT NULL, + `PREV_FIRE_TIME` bigint DEFAULT NULL, + `PRIORITY` int DEFAULT NULL, + `TRIGGER_STATE` varchar(16) NOT NULL, + `TRIGGER_TYPE` varchar(8) NOT NULL, + `START_TIME` bigint NOT NULL, + `END_TIME` bigint DEFAULT NULL, + `CALENDAR_NAME` varchar(200) DEFAULT NULL, + `MISFIRE_INSTR` smallint DEFAULT NULL, + `JOB_DATA` blob, + PRIMARY KEY (`SCHED_NAME`,`TRIGGER_NAME`,`TRIGGER_GROUP`) USING BTREE, + KEY `SCHED_NAME` (`SCHED_NAME`,`JOB_NAME`,`JOB_GROUP`) USING BTREE, + CONSTRAINT `QRTZ_TRIGGERS_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `qrtz_job_details` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC; + -- ---------------------------- -- Table structure for v2_ai_conversations -- ---------------------------- @@ -2260,7 +2433,7 @@ CREATE TABLE `v2_calculator_results` ( KEY `idx_user_id` (`user_id`), KEY `idx_is_adopted` (`is_adopted`), KEY `idx_created_at` (`created_at`) -) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='食谱计算器结果表'; +) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='食谱计算器结果表'; -- ---------------------------- -- Table structure for v2_community_comments @@ -2280,7 +2453,7 @@ CREATE TABLE `v2_community_comments` ( KEY `idx_post_id` (`post_id`), KEY `idx_user_id` (`user_id`), KEY `idx_parent_comment_id` (`parent_comment_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='社区评论表'; +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='社区评论表'; -- ---------------------------- -- Table structure for v2_community_follows @@ -2295,7 +2468,7 @@ CREATE TABLE `v2_community_follows` ( UNIQUE KEY `uk_follower_followee` (`follower_id`,`followee_id`), KEY `idx_follower_id` (`follower_id`), KEY `idx_followee_id` (`followee_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='关注关系表'; +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='关注关系表'; -- ---------------------------- -- Table structure for v2_community_interactions @@ -2311,7 +2484,7 @@ CREATE TABLE `v2_community_interactions` ( UNIQUE KEY `uk_user_post_type` (`user_id`,`post_id`,`interaction_type`), KEY `idx_post_id` (`post_id`), KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='社区互动表'; +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='社区互动表'; -- ---------------------------- -- Table structure for v2_community_posts @@ -2355,7 +2528,24 @@ CREATE TABLE `v2_community_posts` ( KEY `idx_created_at` (`created_at`), KEY `idx_recommend_score` (`recommend_score`), KEY `idx_hot_score` (`hot_score`) -) ENGINE=InnoDB AUTO_INCREMENT=303 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='社区内容表'; +) ENGINE=InnoDB AUTO_INCREMENT=306 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='社区内容表'; + +-- ---------------------------- +-- Table structure for v2_dish_image_cache +-- ---------------------------- +DROP TABLE IF EXISTS `v2_dish_image_cache`; +CREATE TABLE `v2_dish_image_cache` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `dish_name` varchar(100) NOT NULL COMMENT '菜品名称', + `original_url` varchar(500) DEFAULT NULL COMMENT '原始图片URL', + `oss_url` varchar(500) NOT NULL COMMENT 'OSS有效图片URL', + `ai_provider` varchar(50) DEFAULT 'kieai' COMMENT 'AI生成来源', + `task_id` varchar(100) DEFAULT NULL COMMENT 'KieAI任务ID', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_dish_name` (`dish_name`) +) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜品图片缓存表'; -- ---------------------------- -- Table structure for v2_foods @@ -2461,7 +2651,7 @@ CREATE TABLE `v2_nutrition_plans` ( KEY `idx_user_id` (`user_id`), KEY `idx_status` (`status`), KEY `idx_start_date` (`start_date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='营养计划表'; +) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='营养计划表'; -- ---------------------------- -- Table structure for v2_nutritionist_consultations @@ -2560,6 +2750,8 @@ CREATE TABLE `v2_recipes` ( `is_recommend` tinyint(1) DEFAULT '0' COMMENT '是否推荐:0=否,1=是', `is_official` tinyint(1) DEFAULT '0' COMMENT '是否官方食谱:0=否,1=是', `sort_order` int DEFAULT '0' COMMENT '排序', + `source` varchar(20) DEFAULT 'manual' COMMENT '来源:manual(手动)/calculator(计算器)/ai(AI生成)', + `source_id` bigint DEFAULT NULL COMMENT '来源ID(如计算器结果ID)', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`recipe_id`), @@ -2569,7 +2761,7 @@ CREATE TABLE `v2_recipes` ( KEY `idx_status` (`status`), KEY `idx_is_recommend` (`is_recommend`), KEY `idx_created_at` (`created_at`) -) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='食谱表'; +) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='食谱表'; -- ---------------------------- -- Table structure for v2_user_points diff --git a/msh_crmeb_22/.mvn/wrapper/maven-wrapper.properties b/msh_crmeb_22/.mvn/wrapper/maven-wrapper.properties index 1becba2..096a2aa 100644 --- a/msh_crmeb_22/.mvn/wrapper/maven-wrapper.properties +++ b/msh_crmeb_22/.mvn/wrapper/maven-wrapper.properties @@ -1 +1 @@ -404: Not Found \ No newline at end of file +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.8/apache-maven-3.8.8-bin.zip diff --git a/msh_crmeb_22/crmeb-admin/src/main/resources/application-sophia.yml b/msh_crmeb_22/crmeb-admin/src/main/resources/application-sophia.yml index 7ca34a8..648b205 100644 --- a/msh_crmeb_22/crmeb-admin/src/main/resources/application-sophia.yml +++ b/msh_crmeb_22/crmeb-admin/src/main/resources/application-sophia.yml @@ -8,17 +8,19 @@ server: spring: datasource: - name: shop_msh + name: shop-msh + type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver - url: jdbc:mysql://118.89.113.119:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8 - username: baisui - password: fFmTJhBEFSnYGYW7 + url: jdbc:mysql://49.235.131.69:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8 + username: root + password: mogu2018 + redis: - host: 118.89.113.119 #地址 + host: 49.235.131.69 #地址 118.89.113.119 49.235.131.69 port: 6379 #端口 - password: 'UthinkCloud2017' + password: 'mogu2018' timeout: 10000 # 连接超时时间(毫秒) - database: 26 #默认数据库 + database: 3 #默认数据库 jedis: pool: max-active: 200 # 连接池最大连接数(使用负值表示没有限制) @@ -35,7 +37,7 @@ logging: org.springframework.boot.autoconfigure: ERROR config: classpath:logback-spring.xml file: - path: ./crmeb_log + path: ./logs # mybatis 配置 mybatis-plus: diff --git a/msh_crmeb_22/crmeb-admin/src/main/resources/application.yml b/msh_crmeb_22/crmeb-admin/src/main/resources/application.yml index 6ba2511..037f9c3 100644 --- a/msh_crmeb_22/crmeb-admin/src/main/resources/application.yml +++ b/msh_crmeb_22/crmeb-admin/src/main/resources/application.yml @@ -38,7 +38,7 @@ server: spring: profiles: - active: jxz + active: sophia servlet: multipart: max-file-size: 50MB #设置单个文件大小 diff --git a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/result/ResultAdvice.java b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/result/ResultAdvice.java index 1f7fd09..022c09e 100644 --- a/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/result/ResultAdvice.java +++ b/msh_crmeb_22/crmeb-common/src/main/java/com/zbkj/common/result/ResultAdvice.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.servlet.http.HttpServletRequest; import java.util.Objects; @@ -39,6 +40,10 @@ public class ResultAdvice implements ResponseBodyAdvice { */ @Override public boolean supports(MethodParameter returnType, Class> converterType) { + // SseEmitter 由 Spring 内部直接处理,不能经过统一响应包装 + if (SseEmitter.class.isAssignableFrom(returnType.getParameterType())) { + return false; + } ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(sra).getRequest(); CustomResponseAnnotation customResponseAnnotation = (CustomResponseAnnotation) request.getAttribute(CUSTOM_RESPONSE_RESULT_ANNOTATION); diff --git a/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/CozeController.java b/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/CozeController.java index d6126de..da7d0ef 100644 --- a/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/CozeController.java +++ b/msh_crmeb_22/crmeb-front/src/main/java/com/zbkj/front/controller/CozeController.java @@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.IOException; @@ -56,7 +57,9 @@ public class CozeController { */ @ApiOperation(value = "流式对话", notes = "与 Coze Bot 进行流式对话,使用 SSE 实时推送响应") @PostMapping(value = "/chat/stream", produces = "text/event-stream") - public SseEmitter chatStream(@RequestBody CozeChatRequest request) { + public SseEmitter chatStream(@RequestBody CozeChatRequest request, HttpServletResponse response) { + response.setHeader("X-Accel-Buffering", "no"); + response.setHeader("Cache-Control", "no-cache"); request.setStream(true); return toolCozeService.chatStream(request); } diff --git a/msh_crmeb_22/crmeb-front/src/main/resources/application-sophia.yml b/msh_crmeb_22/crmeb-front/src/main/resources/application-sophia.yml index d92c38d..bec4ab6 100644 --- a/msh_crmeb_22/crmeb-front/src/main/resources/application-sophia.yml +++ b/msh_crmeb_22/crmeb-front/src/main/resources/application-sophia.yml @@ -73,7 +73,7 @@ coze: api: base-url: https://api.coze.cn auth-type: pat # pat 或 jwt - token: pat_fGSD3Jax9VNWOJ7yrjke8R1XjeLWQCT2amc2gk4xBI68OPrnlFGwkOAMS2xk5XuY # 有效期30天 + token: pat_ehJTZT6rpqgllqiTmoeOZVRmvsLX9TMq7eVrE3E0q0HcyYQmSCqPNII8vwoaU4EW # 有效期30天 # JWT 模式配置(当 auth-type=jwt 时使用) client-id: 1180790412263 private-key-file: classpath:coze-1180790412263-private_key.pem diff --git a/msh_crmeb_22/crmeb-front/src/main/resources/application.yml b/msh_crmeb_22/crmeb-front/src/main/resources/application.yml index a561ac9..b8ccc0b 100644 --- a/msh_crmeb_22/crmeb-front/src/main/resources/application.yml +++ b/msh_crmeb_22/crmeb-front/src/main/resources/application.yml @@ -1,6 +1,6 @@ # CRMEB 相关配置 crmeb: - version: JAVA-SY-v2.2 # 当前代码版本 + version: SY-v2.2 # 当前代码版本 imagePath: /usr/local/crmeb/crmebimage/ # 服务器图片路径配置 斜杠结尾 asyncConfig: true #是否同步config表数据到redis activityStyleCachedTime: 10 #活动边框缓存周期 秒为单位,生产环境适当5-10分钟即可 diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCozeServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCozeServiceImpl.java index 706d8b4..42fa131 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCozeServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolCozeServiceImpl.java @@ -39,7 +39,12 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** @@ -145,15 +150,30 @@ public class ToolCozeServiceImpl implements ToolCozeService { } } + private static final ScheduledExecutorService heartbeatScheduler = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "sse-heartbeat"); + t.setDaemon(true); + return t; + }); + @Override public SseEmitter chatStream(CozeChatRequest request) { - SseEmitter emitter = new SseEmitter(60000L); // 60 秒超时 + SseEmitter emitter = new SseEmitter(120000L); + + ScheduledFuture heartbeat = heartbeatScheduler.scheduleAtFixedRate(() -> { + try { + emitter.send(SseEmitter.event().comment("heartbeat")); + } catch (Exception ignored) { + } + }, 15, 15, TimeUnit.SECONDS); try { CozeAPI client = getClient(); List messages = buildMessages(request); if (messages == null || messages.isEmpty()) { logger.warn("Coze chat stream: no user message in request"); + heartbeat.cancel(false); emitter.completeWithError(new RuntimeException("请提供对话内容")); return emitter; } @@ -163,7 +183,6 @@ public class ToolCozeServiceImpl implements ToolCozeService { .userID(request.getUserId()) .messages(messages); - // 传入 conversationId 以支持多轮对话上下文 if (request.getConversationId() != null && !request.getConversationId().isEmpty()) { builder.conversationID(request.getConversationId()); } @@ -172,26 +191,59 @@ public class ToolCozeServiceImpl implements ToolCozeService { Disposable disposable = client.chat().stream(req) .subscribe( - chatEvent -> SseEmitterUtil.send(emitter, chatEvent), + chatEvent -> { + Map simplified = new HashMap<>(); + simplified.put("event", chatEvent.getEvent() != null + ? chatEvent.getEvent().getValue() : null); + if (chatEvent.getChat() != null) { + simplified.put("conversation_id", + chatEvent.getChat().getConversationID()); + simplified.put("chat_id", chatEvent.getChat().getID()); + if (chatEvent.getChat().getStatus() != null) { + simplified.put("status", + chatEvent.getChat().getStatus().getValue()); + } + } + if (chatEvent.getMessage() != null) { + simplified.put("content", + chatEvent.getMessage().getContent()); + if (chatEvent.getMessage().getRole() != null) { + simplified.put("role", + chatEvent.getMessage().getRole().getValue()); + } + if (chatEvent.getMessage().getType() != null) { + simplified.put("type", + chatEvent.getMessage().getType().getValue()); + } + } + SseEmitterUtil.send(emitter, simplified); + }, error -> { logger.error("Coze chat stream error", error); + heartbeat.cancel(false); emitter.completeWithError(error); }, - () -> SseEmitterUtil.complete(emitter) + () -> { + heartbeat.cancel(false); + SseEmitterUtil.complete(emitter); + } ); emitter.onCompletion(() -> { + heartbeat.cancel(false); if (!disposable.isDisposed()) { disposable.dispose(); } }); emitter.onTimeout(() -> { + heartbeat.cancel(false); if (!disposable.isDisposed()) { disposable.dispose(); } }); } catch (Exception e) { logger.error("Coze chat stream error", e); + heartbeat.cancel(false); emitter.completeWithError(e); } diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java index 031db27..9883e0a 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java @@ -202,7 +202,7 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService { existQuery.eq(V2Knowledge::getType, "nutrients") .eq(V2Knowledge::getNutrientName, nutrient) .eq(V2Knowledge::getStatus, "published"); - Long count = v2KnowledgeDao.selectCount(existQuery); + Long count = Long.valueOf(v2KnowledgeDao.selectCount(existQuery)); if (count > 0) { log.info("[generateNutrient] 营养素 {} 已存在,跳过", nutrient); continue; @@ -218,7 +218,8 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService { msg.setRole("user"); msg.setContent(prompt); msg.setContentType("text"); - req.setAdditionalMessages(java.util.Collections.singletonList(msg)); + // 修复后 + req.setChatHistory(java.util.Collections.singletonList(msg)); CozeBaseResponse resp = toolCozeService.chat(req); String content = extractCozeContent(resp); diff --git a/msh_single_uniapp/api/models-api.js b/msh_single_uniapp/api/models-api.js index 162df30..460ed6e 100644 --- a/msh_single_uniapp/api/models-api.js +++ b/msh_single_uniapp/api/models-api.js @@ -353,6 +353,114 @@ function cozeChat(data) { }) } +/** + * Coze - 流式对话 (Chat Stream via SSE + enableChunked) + * 使用微信小程序 enableChunked 能力消费 SSE 事件流 + * @param {object} data 请求参数(与 cozeChat 一致) + * @returns {object} 控制器 { onMessage, onError, onComplete, abort, getTask } + */ +function cozeChatStream(data) { + let _onMessage = () => {} + let _onError = () => {} + let _onComplete = () => {} + let _buffer = '' + let _task = null + let _gotChunks = false + + const controller = { + onMessage(fn) { _onMessage = fn; return controller }, + onError(fn) { _onError = fn; return controller }, + onComplete(fn) { _onComplete = fn; return controller }, + abort() { if (_task) _task.abort() }, + getTask() { return _task } + } + + const parseSseLines = (text) => { + _buffer += text + const lines = _buffer.split('\n') + _buffer = lines.pop() || '' + + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith(':')) continue + if (trimmed.startsWith('data:')) { + const jsonStr = trimmed.slice(5).trim() + if (!jsonStr) continue + try { + const evt = JSON.parse(jsonStr) + _onMessage(evt) + } catch (e) { + // skip malformed JSON fragments + } + } + } + } + + const parseSseResponseBody = (body) => { + if (!body || typeof body !== 'string') return + const lines = body.split('\n') + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith(':')) continue + if (trimmed.startsWith('data:')) { + const jsonStr = trimmed.slice(5).trim() + if (!jsonStr) continue + try { + const evt = JSON.parse(jsonStr) + _onMessage(evt) + } catch (e) { + // skip malformed JSON fragments + } + } + } + } + + const token = uni.getStorageSync('LOGIN_STATUS_TOKEN') || '' + _task = uni.request({ + url: `${API_BASE_URL}/api/front/coze/chat/stream`, + method: 'POST', + data: data, + header: { + 'Content-Type': 'application/json', + ...(token ? { 'Authori-zation': token } : {}) + }, + enableChunked: true, + responseType: 'text', + success: (res) => { + if (_buffer.trim()) { + parseSseLines('\n') + } + if (!_gotChunks && res && res.data) { + const body = typeof res.data === 'string' ? res.data : JSON.stringify(res.data) + parseSseResponseBody(body) + } + _onComplete() + }, + fail: (err) => { + _onError(err) + } + }) + + if (_task && _task.onChunkReceived) { + _task.onChunkReceived((res) => { + _gotChunks = true + try { + const bytes = new Uint8Array(res.data) + let text = '' + for (let i = 0; i < bytes.length; i++) { + text += String.fromCharCode(bytes[i]) + } + text = decodeURIComponent(escape(text)) + parseSseLines(text) + } catch (e) { + // chunk decode error, skip + } + }) + } + + return controller +} + /** * Coze - 检索对话详情 (Retrieve Chat) * @param {object} params 请求参数 @@ -471,6 +579,7 @@ export default { kieaiGeminiChat, // Coze API cozeChat, + cozeChatStream, cozeRetrieveChat, cozeMessageList, cozeWorkflowRun, diff --git a/msh_single_uniapp/config/app.js b/msh_single_uniapp/config/app.js index b8caa5f..c5d83a5 100644 --- a/msh_single_uniapp/config/app.js +++ b/msh_single_uniapp/config/app.js @@ -2,9 +2,9 @@ // | // +---------------------------------------------------------------------- // 移动端商城API -// let domain = 'http://127.0.0.1:20822' +let domain = 'http://127.0.0.1:20822' // let domain = 'https://chenyin.uj345.cc' -let domain = 'https://sophia-shop.uj345.cc' +// let domain = 'https://sophia-shop.uj345.cc' module.exports = { domain, diff --git a/msh_single_uniapp/pages/tool/ai-nutritionist.vue b/msh_single_uniapp/pages/tool/ai-nutritionist.vue index c9dec8a..332c19d 100644 --- a/msh_single_uniapp/pages/tool/ai-nutritionist.vue +++ b/msh_single_uniapp/pages/tool/ai-nutritionist.vue @@ -63,13 +63,13 @@ - - - - - - - {{ msg.content }} + + + + + + + {{ msg.content }}| - - + + 🤖 @@ -236,6 +236,10 @@ export default { if (this.isRecording && this.recorderManager) { this.recorderManager.stop(); } + if (this._streamCtrl) { + this._streamCtrl.abort(); + this._streamCtrl = null; + } }, methods: { // 初始化录音管理器 @@ -541,8 +545,13 @@ export default { content: '确定要清空对话吗?', success: (res) => { if (res.confirm) { - this.messageList = [] - this.conversationId = '' // 清空会话ID,开始新的对话 + if (this._streamCtrl) { + this._streamCtrl.abort(); + this._streamCtrl = null; + } + this.isLoading = false; + this.messageList = []; + this.conversationId = ''; } } }) @@ -650,79 +659,164 @@ export default { return data; }, + buildCozeMessages(content, type) { + const messages = []; + if (type === 'text') { + messages.push({ + role: 'user', + content: typeof content === 'string' ? content : JSON.stringify(content), + content_type: 'text' + }); + } else if (type === 'multimodal') { + const parts = Array.isArray(content) ? content : [{ type: 'text', text: String(content) }]; + const textPart = parts.find(p => p && p.type === 'text'); + const imgPart = parts.find(p => p && (p.type === 'image_url' || p.type === 'image')); + if (imgPart) { + const fileId = imgPart.file_id || (imgPart.image_url && imgPart.image_url.url) || ''; + messages.push({ + role: 'user', + content: JSON.stringify([ + ...(textPart ? [{ type: 'text', text: textPart.text }] : []), + { type: 'image', file_id: fileId } + ]), + content_type: 'object_string' + }); + } else { + messages.push({ + role: 'user', + content: textPart ? textPart.text : JSON.stringify(content), + content_type: 'text' + }); + } + } else { + let fileInfo = content; + if (typeof fileInfo === 'string') { + try { fileInfo = JSON.parse(fileInfo); } catch (e) { /* ignore */ } + } + const fileId = (fileInfo && fileInfo.id) || (fileInfo && fileInfo.file_id) || ''; + if (fileId) { + messages.push({ + role: 'user', + content: JSON.stringify([{ type: 'image', file_id: fileId }]), + content_type: 'object_string' + }); + } else { + messages.push({ + role: 'user', + content: '我发送了一张图片,请帮我分析', + content_type: 'text' + }); + } + } + return messages; + }, + + supportsChunked() { + try { + const sysInfo = uni.getSystemInfoSync(); + if (sysInfo.SDKVersion) { + const parts = sysInfo.SDKVersion.split('.').map(Number); + return (parts[0] > 2) || (parts[0] === 2 && parts[1] > 20) || + (parts[0] === 2 && parts[1] === 20 && (parts[2] || 0) >= 1); + } + } catch (e) { /* fallback */ } + return false; + }, + async sendToAI(content, type) { this.isLoading = true; - // 添加 AI 占位消息(loading 状态,等待 Coze 返回后填充内容) - const aiMsg = { role: 'ai', content: '', loading: true }; + const aiMsg = { role: 'ai', content: '', loading: true, streaming: false }; this.messageList.push(aiMsg); this.scrollToBottom(); - // 统一走 Coze API(文本、多模态、图片均使用 Coze Bot) const userId = this.uid || (uni.getStorageSync('userInfo') || {}).id || 'default_user'; + const messages = this.buildCozeMessages(content, type); + + const requestData = { + botId: this.botId, + userId: userId, + additionalMessages: messages, + stream: true, + autoSaveHistory: true + }; + if (this.conversationId) requestData.conversationId = this.conversationId; + + if (this.supportsChunked()) { + this.sendToAIStream(requestData, aiMsg); + } else { + this.sendToAIPoll(requestData, aiMsg); + } + }, + + sendToAIStream(requestData, aiMsg) { + const ctrl = api.cozeChatStream(requestData) + .onMessage((evt) => { + const eventType = evt.event || ''; + + if (eventType === 'conversation.chat.created') { + if (evt.conversation_id) { + this.conversationId = evt.conversation_id; + } + } else if (eventType === 'conversation.message.delta') { + const role = evt.role || 'assistant'; + const type = evt.type || 'answer'; + if (role === 'assistant' && type === 'answer') { + if (aiMsg.loading) { + aiMsg.loading = false; + aiMsg.streaming = true; + } + aiMsg.content += (evt.content || ''); + this.messageList = [...this.messageList]; + this.scrollToBottom(); + } + } else if (eventType === 'conversation.chat.completed') { + aiMsg.streaming = false; + this.isLoading = false; + this.messageList = [...this.messageList]; + this.scrollToBottom(); + } else if (eventType === 'conversation.chat.failed') { + aiMsg.loading = false; + aiMsg.streaming = false; + if (!aiMsg.content) { + aiMsg.content = '抱歉,AI 对话失败,请稍后再试。'; + } + this.isLoading = false; + this.messageList = [...this.messageList]; + this.scrollToBottom(); + } + }) + .onError((err) => { + console.error('SSE 流式对话失败:', err); + aiMsg.loading = false; + aiMsg.streaming = false; + if (!aiMsg.content) { + aiMsg.content = '抱歉,处理您的请求时出现错误,请稍后再试。'; + } + this.isLoading = false; + this.messageList = [...this.messageList]; + this.scrollToBottom(); + }) + .onComplete(() => { + aiMsg.loading = false; + aiMsg.streaming = false; + if (!aiMsg.content) { + aiMsg.content = '未能获取到有效回复。'; + } + this.isLoading = false; + this.messageList = [...this.messageList]; + this.scrollToBottom(); + }); + + this._streamCtrl = ctrl; + }, + + async sendToAIPoll(requestData, aiMsg) { + requestData.stream = false; try { - const messages = []; - if (type === 'text') { - // 纯文字消息 - messages.push({ - role: 'user', - content: typeof content === 'string' ? content : JSON.stringify(content), - content_type: 'text' - }); - } else if (type === 'multimodal') { - // 图文混合:content 为 parts 数组 [{ type: 'text', text }, { type: 'image_url', ... }] - const parts = Array.isArray(content) ? content : [{ type: 'text', text: String(content) }]; - const textPart = parts.find(p => p && p.type === 'text'); - const imgPart = parts.find(p => p && (p.type === 'image_url' || p.type === 'image')); - if (imgPart) { - const fileId = imgPart.file_id || (imgPart.image_url && imgPart.image_url.url) || ''; - messages.push({ - role: 'user', - content: JSON.stringify([ - ...(textPart ? [{ type: 'text', text: textPart.text }] : []), - { type: 'image', file_id: fileId } - ]), - content_type: 'object_string' - }); - } else { - messages.push({ - role: 'user', - content: textPart ? textPart.text : JSON.stringify(content), - content_type: 'text' - }); - } - } else { - // 图片路径(旧路径,content 为 fileInfo 对象) - let fileInfo = content; - if (typeof fileInfo === 'string') { - try { fileInfo = JSON.parse(fileInfo); } catch (e) { /* 非JSON */ } - } - const fileId = (fileInfo && fileInfo.id) || (fileInfo && fileInfo.file_id) || ''; - if (fileId) { - messages.push({ - role: 'user', - content: JSON.stringify([{ type: 'image', file_id: fileId }]), - content_type: 'object_string' - }); - } else { - messages.push({ - role: 'user', - content: '我发送了一张图片,请帮我分析', - content_type: 'text' - }); - } - } - const requestData = { - botId: this.botId, - userId: userId, - additionalMessages: messages, - stream: false, - autoSaveHistory: true - }; - if (this.conversationId) requestData.conversationId = this.conversationId; - const response = await api.cozeChat(requestData); - const cozeData = this.unwrapCozeResponse(response); - if (cozeData) { + const response = await api.cozeChat(requestData); + const cozeData = this.unwrapCozeResponse(response); + if (cozeData) { const chat = cozeData.chat || cozeData; const conversationId = chat.conversation_id || chat.conversationID || chat.conversationId; const chatId = chat.id; @@ -741,13 +835,19 @@ export default { this.isLoading = false; aiMsg.content = '抱歉,处理您的请求时出现错误,请稍后再试。'; aiMsg.loading = false; - this.messageList = [...this.messageList]; // 触发响应式更新 + this.messageList = [...this.messageList]; this.scrollToBottom(); } }, + getPollInterval(attempt) { + if (attempt <= 10) return 500; + if (attempt <= 30) return 1000; + return 1500; + }, + async pollChatStatus(conversationId, chatId, aiMsg) { - const maxAttempts = 60; // 最多轮询60次(每次1.5秒),即90秒 + const maxAttempts = 80; let attempts = 0; const checkStatus = async () => { @@ -761,37 +861,33 @@ export default { } try { - const res = await api.cozeRetrieveChat({ - conversationId, - chatId - }); - - const retrieveData = this.unwrapCozeResponse(res); - if (retrieveData) { - console.log("====api.cozeRetrieveChat response====", retrieveData); - const chatObj = retrieveData.chat || retrieveData; - const status = chatObj && chatObj.status; - - if (status === 'completed') { - // 对话完成,获取消息详情 - await this.getChatMessages(conversationId, chatId, aiMsg); - } else if (status === 'failed' || status === 'canceled') { - this.isLoading = false; - const failMsg = `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}。`; - if (aiMsg) { aiMsg.content = failMsg; aiMsg.loading = false; this.messageList = [...this.messageList]; } - else { this.messageList.push({ role: 'ai', content: failMsg }); } - this.scrollToBottom(); + const res = await api.cozeRetrieveChat({ + conversationId, + chatId + }); + + const retrieveData = this.unwrapCozeResponse(res); + if (retrieveData) { + const chatObj = retrieveData.chat || retrieveData; + const status = chatObj && chatObj.status; + + if (status === 'completed') { + await this.getChatMessages(conversationId, chatId, aiMsg); + } else if (status === 'failed' || status === 'canceled') { + this.isLoading = false; + const failMsg = `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}。`; + if (aiMsg) { aiMsg.content = failMsg; aiMsg.loading = false; this.messageList = [...this.messageList]; } + else { this.messageList.push({ role: 'ai', content: failMsg }); } + this.scrollToBottom(); + } else { + setTimeout(checkStatus, this.getPollInterval(attempts)); + } } else { - // 继续轮询 (created, in_progress) 每 1.5 秒 - setTimeout(checkStatus, 1500); - } - } else { - // 查询失败,重试 - setTimeout(checkStatus, 1000); + setTimeout(checkStatus, this.getPollInterval(attempts)); } } catch (e) { console.error('查询对话状态失败:', e); - setTimeout(checkStatus, 1000); + setTimeout(checkStatus, this.getPollInterval(attempts)); } }; @@ -1088,6 +1184,18 @@ export default { } } +/* 流式输出闪烁光标 */ +.streaming-cursor { + animation: blink 0.8s step-end infinite; + color: #4facfe; + font-weight: 600; +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + /* 打字指示器 */ .typing-indicator { display: flex; diff --git a/msh_single_uniapp/pages/tool/food-encyclopedia.vue b/msh_single_uniapp/pages/tool/food-encyclopedia.vue index d432d60..5a51a03 100644 --- a/msh_single_uniapp/pages/tool/food-encyclopedia.vue +++ b/msh_single_uniapp/pages/tool/food-encyclopedia.vue @@ -119,9 +119,9 @@ {{ item.safety || '—' }} - +