fix: 移除损坏的 Claude gitlink 并同步业务与文档更新

- 从索引移除误记录的 .claude/worktrees gitlink(旧绝对路径会导致 git 命令失败)
- 新增根目录 .gitignore 忽略 .claude/worktrees 与 .DS_Store
- 后端:Coze/知识库、ResultAdvice、应用配置
- 前端 uniapp:AI 营养、食物百科等页面与 API
- 更新 README、测试文档与 shop-msh.sql

Made-with: Cursor
This commit is contained in:
panchengyong
2026-03-30 12:46:24 +08:00
parent 3329a2b296
commit 3023115bb0
19 changed files with 671 additions and 166 deletions

Submodule .claude/worktrees/hopeful-goldberg deleted from c69ce2891f

Submodule .claude/worktrees/suspicious-antonelli deleted from 6f2dc27fbc

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.DS_Store
.claude/worktrees/

View File

@@ -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
---
## 文档索引 ## 文档索引
| 文档 | 路径 | | 文档 | 路径 |

View File

@@ -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 ## 页面pages/tool/ai-nutritionist
- 1. 优化方案:/Users/a123/msh-system/docs/功能开发详细设计_2026-03-25.md - 1. 请求后页面显示:"未能获取到有效回复。"
- 2. 对话响应还是很慢是否可以使用SSE流式对话来优化响应速度 fetch("http://127.0.0.1:20822/api/front/coze/chat/stream", {
- 3. **已修复** 会话错误:"发起对话失败未返回会话或对话ID" "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 - 1. /Users/a123/msh-system/docs/测试问题分析报告_2026-03-22.md
- 2. /Users/a123/msh-system/docs/功能开发详细设计_2026-03-25.md - 2. /Users/a123/msh-system/docs/功能开发详细设计_2026-03-25.md

View File

@@ -11,7 +11,7 @@
Target Server Version : 80022 (8.0.22) Target Server Version : 80022 (8.0.22)
File Encoding : 65001 File Encoding : 65001
Date: 01/02/2026 22:29:26 Date: 25/03/2026 11:49:10
*/ */
SET NAMES utf8mb4; SET NAMES utf8mb4;
@@ -158,7 +158,7 @@ CREATE TABLE `eb_article` (
KEY `idx_post_id` (`post_id`), KEY `idx_post_id` (`post_id`),
KEY `idx_check_in_record_id` (`check_in_record_id`), KEY `idx_check_in_record_id` (`check_in_record_id`),
KEY `idx_type_status_task` (`type`,`status_task`) 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 -- Table structure for eb_category
@@ -195,7 +195,7 @@ CREATE TABLE `eb_exception_log` (
`exp_detail` longtext COMMENT '异常详细信息', `exp_detail` longtext COMMENT '异常详细信息',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE 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 -- 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 '销售额', `order_success_product_fee` decimal(8,2) DEFAULT NULL COMMENT '销售额',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
KEY `date` (`date`) 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 -- Table structure for eb_schedule_job
@@ -611,7 +611,7 @@ CREATE TABLE `eb_schedule_job_log` (
`times` int NOT NULL COMMENT '耗时(单位:毫秒)', `times` int NOT NULL COMMENT '耗时(单位:毫秒)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间', `create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`log_id`) USING BTREE 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 -- 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 '交易成功商品数', `order_success_product_num` int DEFAULT NULL COMMENT '交易成功商品数',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
KEY `date` (`date`) 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 -- Table structure for eb_sms_record
@@ -1448,7 +1448,7 @@ CREATE TABLE `eb_system_attachment` (
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`att_id`) USING BTREE 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 -- Table structure for eb_system_city
@@ -1693,7 +1693,7 @@ CREATE TABLE `eb_trading_day_record` (
`brokerage_fee` decimal(8,2) DEFAULT NULL COMMENT '支付佣金金额(用户确认到账佣金)', `brokerage_fee` decimal(8,2) DEFAULT NULL COMMENT '支付佣金金额(用户确认到账佣金)',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
KEY `date` (`date`) 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 -- Table structure for eb_user
@@ -1747,7 +1747,7 @@ CREATE TABLE `eb_user` (
KEY `level` (`level`) USING BTREE, KEY `level` (`level`) USING BTREE,
KEY `status` (`status`) USING BTREE, KEY `status` (`status`) USING BTREE,
KEY `is_promoter` (`is_promoter`) 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 -- Table structure for eb_user_address
@@ -1853,7 +1853,7 @@ CREATE TABLE `eb_user_experience_record` (
KEY `add_time` (`create_time`) USING BTREE, KEY `add_time` (`create_time`) USING BTREE,
KEY `type` (`type`) USING BTREE, KEY `type` (`type`) USING BTREE,
KEY `type_link` (`type`,`link_id`) 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 -- Table structure for eb_user_extract
@@ -1921,7 +1921,7 @@ CREATE TABLE `eb_user_integral_record` (
KEY `type` (`type`) USING BTREE, KEY `type` (`type`) USING BTREE,
KEY `type_link` (`type`,`link_id`) USING BTREE, KEY `type_link` (`type`,`link_id`) USING BTREE,
KEY `idx_source_detail` (`source_detail`) 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 -- Table structure for eb_user_level
@@ -1942,7 +1942,7 @@ CREATE TABLE `eb_user_level` (
`expired_time` timestamp NULL DEFAULT NULL COMMENT '过期时间', `expired_time` timestamp NULL DEFAULT NULL COMMENT '过期时间',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `id` (`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 -- Table structure for eb_user_recharge
@@ -2020,7 +2020,7 @@ CREATE TABLE `eb_user_sign` (
KEY `idx_report_id` (`report_id`), KEY `idx_report_id` (`report_id`),
KEY `idx_nutrition_score` (`nutrition_score`), KEY `idx_nutrition_score` (`nutrition_score`),
KEY `idx_copied_from` (`copied_from_sign_id`) 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 -- Table structure for eb_user_tag
@@ -2046,7 +2046,7 @@ CREATE TABLE `eb_user_token` (
`login_ip` varchar(32) DEFAULT NULL COMMENT '登录ip', `login_ip` varchar(32) DEFAULT NULL COMMENT '登录ip',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `type+token` (`type`,`token`) 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 -- Table structure for eb_user_visit_record
@@ -2059,7 +2059,7 @@ CREATE TABLE `eb_user_visit_record` (
`visit_type` int DEFAULT NULL COMMENT '访问类型', `visit_type` int DEFAULT NULL COMMENT '访问类型',
PRIMARY KEY (`id`) USING BTREE, PRIMARY KEY (`id`) USING BTREE,
KEY `date` (`date`) 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 -- Table structure for eb_wechat_callback
@@ -2090,7 +2090,7 @@ CREATE TABLE `eb_wechat_exceptions` (
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE 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 -- Table structure for eb_wechat_pay_info
@@ -2186,6 +2186,179 @@ CREATE TABLE `eb_wechat_reply` (
KEY `status` (`status`) USING BTREE KEY `status` (`status`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='微信关键字回复表'; ) 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 -- Table structure for v2_ai_conversations
-- ---------------------------- -- ----------------------------
@@ -2260,7 +2433,7 @@ CREATE TABLE `v2_calculator_results` (
KEY `idx_user_id` (`user_id`), KEY `idx_user_id` (`user_id`),
KEY `idx_is_adopted` (`is_adopted`), KEY `idx_is_adopted` (`is_adopted`),
KEY `idx_created_at` (`created_at`) 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 -- Table structure for v2_community_comments
@@ -2280,7 +2453,7 @@ CREATE TABLE `v2_community_comments` (
KEY `idx_post_id` (`post_id`), KEY `idx_post_id` (`post_id`),
KEY `idx_user_id` (`user_id`), KEY `idx_user_id` (`user_id`),
KEY `idx_parent_comment_id` (`parent_comment_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 -- Table structure for v2_community_follows
@@ -2295,7 +2468,7 @@ CREATE TABLE `v2_community_follows` (
UNIQUE KEY `uk_follower_followee` (`follower_id`,`followee_id`), UNIQUE KEY `uk_follower_followee` (`follower_id`,`followee_id`),
KEY `idx_follower_id` (`follower_id`), KEY `idx_follower_id` (`follower_id`),
KEY `idx_followee_id` (`followee_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 -- 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`), UNIQUE KEY `uk_user_post_type` (`user_id`,`post_id`,`interaction_type`),
KEY `idx_post_id` (`post_id`), KEY `idx_post_id` (`post_id`),
KEY `idx_user_id` (`user_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 -- Table structure for v2_community_posts
@@ -2355,7 +2528,24 @@ CREATE TABLE `v2_community_posts` (
KEY `idx_created_at` (`created_at`), KEY `idx_created_at` (`created_at`),
KEY `idx_recommend_score` (`recommend_score`), KEY `idx_recommend_score` (`recommend_score`),
KEY `idx_hot_score` (`hot_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 -- Table structure for v2_foods
@@ -2461,7 +2651,7 @@ CREATE TABLE `v2_nutrition_plans` (
KEY `idx_user_id` (`user_id`), KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`), KEY `idx_status` (`status`),
KEY `idx_start_date` (`start_date`) 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 -- Table structure for v2_nutritionist_consultations
@@ -2560,6 +2750,8 @@ CREATE TABLE `v2_recipes` (
`is_recommend` tinyint(1) DEFAULT '0' COMMENT '是否推荐0=否1=是', `is_recommend` tinyint(1) DEFAULT '0' COMMENT '是否推荐0=否1=是',
`is_official` tinyint(1) DEFAULT '0' COMMENT '是否官方食谱0=否1=是', `is_official` tinyint(1) DEFAULT '0' COMMENT '是否官方食谱0=否1=是',
`sort_order` int DEFAULT '0' COMMENT '排序', `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 '创建时间', `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`recipe_id`), PRIMARY KEY (`recipe_id`),
@@ -2569,7 +2761,7 @@ CREATE TABLE `v2_recipes` (
KEY `idx_status` (`status`), KEY `idx_status` (`status`),
KEY `idx_is_recommend` (`is_recommend`), KEY `idx_is_recommend` (`is_recommend`),
KEY `idx_created_at` (`created_at`) 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 -- Table structure for v2_user_points

View File

@@ -1 +1 @@
404: Not Found distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.8/apache-maven-3.8.8-bin.zip

View File

@@ -8,17 +8,19 @@ server:
spring: spring:
datasource: datasource:
name: shop_msh name: shop-msh
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver 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 url: jdbc:mysql://49.235.131.69:3306/${spring.datasource.name}?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf8
username: baisui username: root
password: fFmTJhBEFSnYGYW7 password: mogu2018
redis: redis:
host: 118.89.113.119 #地址 host: 49.235.131.69 #地址 118.89.113.119 49.235.131.69
port: 6379 #端口 port: 6379 #端口
password: 'UthinkCloud2017' password: 'mogu2018'
timeout: 10000 # 连接超时时间(毫秒) timeout: 10000 # 连接超时时间(毫秒)
database: 26 #默认数据库 database: 3 #默认数据库
jedis: jedis:
pool: pool:
max-active: 200 # 连接池最大连接数(使用负值表示没有限制) max-active: 200 # 连接池最大连接数(使用负值表示没有限制)
@@ -35,7 +37,7 @@ logging:
org.springframework.boot.autoconfigure: ERROR org.springframework.boot.autoconfigure: ERROR
config: classpath:logback-spring.xml config: classpath:logback-spring.xml
file: file:
path: ./crmeb_log path: ./logs
# mybatis 配置 # mybatis 配置
mybatis-plus: mybatis-plus:

View File

@@ -38,7 +38,7 @@ server:
spring: spring:
profiles: profiles:
active: jxz active: sophia
servlet: servlet:
multipart: multipart:
max-file-size: 50MB #设置单个文件大小 max-file-size: 50MB #设置单个文件大小

View File

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Objects; import java.util.Objects;
@@ -39,6 +40,10 @@ public class ResultAdvice implements ResponseBodyAdvice<Object> {
*/ */
@Override @Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// SseEmitter 由 Spring 内部直接处理,不能经过统一响应包装
if (SseEmitter.class.isAssignableFrom(returnType.getParameterType())) {
return false;
}
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(sra).getRequest(); HttpServletRequest request = Objects.requireNonNull(sra).getRequest();
CustomResponseAnnotation customResponseAnnotation = (CustomResponseAnnotation) request.getAttribute(CUSTOM_RESPONSE_RESULT_ANNOTATION); CustomResponseAnnotation customResponseAnnotation = (CustomResponseAnnotation) request.getAttribute(CUSTOM_RESPONSE_RESULT_ANNOTATION);

View File

@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletResponse;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -56,7 +57,9 @@ public class CozeController {
*/ */
@ApiOperation(value = "流式对话", notes = "与 Coze Bot 进行流式对话,使用 SSE 实时推送响应") @ApiOperation(value = "流式对话", notes = "与 Coze Bot 进行流式对话,使用 SSE 实时推送响应")
@PostMapping(value = "/chat/stream", produces = "text/event-stream") @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); request.setStream(true);
return toolCozeService.chatStream(request); return toolCozeService.chatStream(request);
} }

View File

@@ -73,7 +73,7 @@ coze:
api: api:
base-url: https://api.coze.cn base-url: https://api.coze.cn
auth-type: pat # pat 或 jwt auth-type: pat # pat 或 jwt
token: pat_fGSD3Jax9VNWOJ7yrjke8R1XjeLWQCT2amc2gk4xBI68OPrnlFGwkOAMS2xk5XuY # 有效期30天 token: pat_ehJTZT6rpqgllqiTmoeOZVRmvsLX9TMq7eVrE3E0q0HcyYQmSCqPNII8vwoaU4EW # 有效期30天
# JWT 模式配置(当 auth-type=jwt 时使用) # JWT 模式配置(当 auth-type=jwt 时使用)
client-id: 1180790412263 client-id: 1180790412263
private-key-file: classpath:coze-1180790412263-private_key.pem private-key-file: classpath:coze-1180790412263-private_key.pem

View File

@@ -1,6 +1,6 @@
# CRMEB 相关配置 # CRMEB 相关配置
crmeb: crmeb:
version: JAVA-SY-v2.2 # 当前代码版本 version: SY-v2.2 # 当前代码版本
imagePath: /usr/local/crmeb/crmebimage/ # 服务器图片路径配置 斜杠结尾 imagePath: /usr/local/crmeb/crmebimage/ # 服务器图片路径配置 斜杠结尾
asyncConfig: true #是否同步config表数据到redis asyncConfig: true #是否同步config表数据到redis
activityStyleCachedTime: 10 #活动边框缓存周期 秒为单位生产环境适当5-10分钟即可 activityStyleCachedTime: 10 #活动边框缓存周期 秒为单位生产环境适当5-10分钟即可

View File

@@ -39,7 +39,12 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; 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; 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 @Override
public SseEmitter chatStream(CozeChatRequest request) { 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 { try {
CozeAPI client = getClient(); CozeAPI client = getClient();
List<Message> messages = buildMessages(request); List<Message> messages = buildMessages(request);
if (messages == null || messages.isEmpty()) { if (messages == null || messages.isEmpty()) {
logger.warn("Coze chat stream: no user message in request"); logger.warn("Coze chat stream: no user message in request");
heartbeat.cancel(false);
emitter.completeWithError(new RuntimeException("请提供对话内容")); emitter.completeWithError(new RuntimeException("请提供对话内容"));
return emitter; return emitter;
} }
@@ -163,7 +183,6 @@ public class ToolCozeServiceImpl implements ToolCozeService {
.userID(request.getUserId()) .userID(request.getUserId())
.messages(messages); .messages(messages);
// 传入 conversationId 以支持多轮对话上下文
if (request.getConversationId() != null && !request.getConversationId().isEmpty()) { if (request.getConversationId() != null && !request.getConversationId().isEmpty()) {
builder.conversationID(request.getConversationId()); builder.conversationID(request.getConversationId());
} }
@@ -172,26 +191,59 @@ public class ToolCozeServiceImpl implements ToolCozeService {
Disposable disposable = client.chat().stream(req) Disposable disposable = client.chat().stream(req)
.subscribe( .subscribe(
chatEvent -> SseEmitterUtil.send(emitter, chatEvent), chatEvent -> {
Map<String, Object> 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 -> { error -> {
logger.error("Coze chat stream error", error); logger.error("Coze chat stream error", error);
heartbeat.cancel(false);
emitter.completeWithError(error); emitter.completeWithError(error);
}, },
() -> SseEmitterUtil.complete(emitter) () -> {
heartbeat.cancel(false);
SseEmitterUtil.complete(emitter);
}
); );
emitter.onCompletion(() -> { emitter.onCompletion(() -> {
heartbeat.cancel(false);
if (!disposable.isDisposed()) { if (!disposable.isDisposed()) {
disposable.dispose(); disposable.dispose();
} }
}); });
emitter.onTimeout(() -> { emitter.onTimeout(() -> {
heartbeat.cancel(false);
if (!disposable.isDisposed()) { if (!disposable.isDisposed()) {
disposable.dispose(); disposable.dispose();
} }
}); });
} catch (Exception e) { } catch (Exception e) {
logger.error("Coze chat stream error", e); logger.error("Coze chat stream error", e);
heartbeat.cancel(false);
emitter.completeWithError(e); emitter.completeWithError(e);
} }

View File

@@ -202,7 +202,7 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService {
existQuery.eq(V2Knowledge::getType, "nutrients") existQuery.eq(V2Knowledge::getType, "nutrients")
.eq(V2Knowledge::getNutrientName, nutrient) .eq(V2Knowledge::getNutrientName, nutrient)
.eq(V2Knowledge::getStatus, "published"); .eq(V2Knowledge::getStatus, "published");
Long count = v2KnowledgeDao.selectCount(existQuery); Long count = Long.valueOf(v2KnowledgeDao.selectCount(existQuery));
if (count > 0) { if (count > 0) {
log.info("[generateNutrient] 营养素 {} 已存在,跳过", nutrient); log.info("[generateNutrient] 营养素 {} 已存在,跳过", nutrient);
continue; continue;
@@ -218,7 +218,8 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService {
msg.setRole("user"); msg.setRole("user");
msg.setContent(prompt); msg.setContent(prompt);
msg.setContentType("text"); msg.setContentType("text");
req.setAdditionalMessages(java.util.Collections.singletonList(msg)); // 修复后
req.setChatHistory(java.util.Collections.singletonList(msg));
CozeBaseResponse<Object> resp = toolCozeService.chat(req); CozeBaseResponse<Object> resp = toolCozeService.chat(req);
String content = extractCozeContent(resp); String content = extractCozeContent(resp);

View File

@@ -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) * Coze - 检索对话详情 (Retrieve Chat)
* @param {object} params 请求参数 * @param {object} params 请求参数
@@ -471,6 +579,7 @@ export default {
kieaiGeminiChat, kieaiGeminiChat,
// Coze API // Coze API
cozeChat, cozeChat,
cozeChatStream,
cozeRetrieveChat, cozeRetrieveChat,
cozeMessageList, cozeMessageList,
cozeWorkflowRun, cozeWorkflowRun,

View File

@@ -2,9 +2,9 @@
// | // |
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
// 移动端商城API // 移动端商城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://chenyin.uj345.cc'
let domain = 'https://sophia-shop.uj345.cc' // let domain = 'https://sophia-shop.uj345.cc'
module.exports = { module.exports = {
domain, domain,

View File

@@ -63,13 +63,13 @@
<!-- 消息气泡 --> <!-- 消息气泡 -->
<view :class="['message-bubble', msg.role === 'user' ? 'user-bubble' : 'ai-bubble']"> <view :class="['message-bubble', msg.role === 'user' ? 'user-bubble' : 'ai-bubble']">
<!-- AI 消息 loading 占位等待回复时显示打字动画--> <!-- AI 消息 loading 占位等待回复时显示打字动画-->
<view v-if="msg.role === 'ai' && msg.loading" class="typing-indicator"> <view v-if="msg.role === 'ai' && msg.loading" class="typing-indicator">
<view class="typing-dot"></view> <view class="typing-dot"></view>
<view class="typing-dot"></view> <view class="typing-dot"></view>
<view class="typing-dot"></view> <view class="typing-dot"></view>
</view> </view>
<text v-else-if="msg.type !== 'image'" class="message-text">{{ msg.content }}</text> <text v-else-if="msg.type !== 'image'" class="message-text">{{ msg.content }}<text v-if="msg.streaming" class="streaming-cursor">|</text></text>
<image <image
v-else v-else
:src="msg.imageUrl" :src="msg.imageUrl"
@@ -81,8 +81,8 @@
</view> </view>
</view> </view>
<!-- 加载中提示 --> <!-- 加载中提示仅在没有流式占位消息时显示 -->
<view v-if="isLoading" class="message-item ai-message"> <view v-if="isLoading && !messageList.some(m => m.loading || m.streaming)" class="message-item ai-message">
<view class="message-avatar"> <view class="message-avatar">
<text class="avatar-icon">🤖</text> <text class="avatar-icon">🤖</text>
</view> </view>
@@ -236,6 +236,10 @@ export default {
if (this.isRecording && this.recorderManager) { if (this.isRecording && this.recorderManager) {
this.recorderManager.stop(); this.recorderManager.stop();
} }
if (this._streamCtrl) {
this._streamCtrl.abort();
this._streamCtrl = null;
}
}, },
methods: { methods: {
// 初始化录音管理器 // 初始化录音管理器
@@ -541,8 +545,13 @@ export default {
content: '确定要清空对话吗?', content: '确定要清空对话吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
this.messageList = [] if (this._streamCtrl) {
this.conversationId = '' // 清空会话ID开始新的对话 this._streamCtrl.abort();
this._streamCtrl = null;
}
this.isLoading = false;
this.messageList = [];
this.conversationId = '';
} }
} }
}) })
@@ -650,79 +659,164 @@ export default {
return data; 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) { async sendToAI(content, type) {
this.isLoading = true; this.isLoading = true;
// 添加 AI 占位消息loading 状态,等待 Coze 返回后填充内容) const aiMsg = { role: 'ai', content: '', loading: true, streaming: false };
const aiMsg = { role: 'ai', content: '', loading: true };
this.messageList.push(aiMsg); this.messageList.push(aiMsg);
this.scrollToBottom(); this.scrollToBottom();
// 统一走 Coze API文本、多模态、图片均使用 Coze Bot
const userId = this.uid || (uni.getStorageSync('userInfo') || {}).id || 'default_user'; 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 { try {
const messages = []; const response = await api.cozeChat(requestData);
if (type === 'text') { const cozeData = this.unwrapCozeResponse(response);
// 纯文字消息 if (cozeData) {
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 chat = cozeData.chat || cozeData; const chat = cozeData.chat || cozeData;
const conversationId = chat.conversation_id || chat.conversationID || chat.conversationId; const conversationId = chat.conversation_id || chat.conversationID || chat.conversationId;
const chatId = chat.id; const chatId = chat.id;
@@ -741,13 +835,19 @@ export default {
this.isLoading = false; this.isLoading = false;
aiMsg.content = '抱歉,处理您的请求时出现错误,请稍后再试。'; aiMsg.content = '抱歉,处理您的请求时出现错误,请稍后再试。';
aiMsg.loading = false; aiMsg.loading = false;
this.messageList = [...this.messageList]; // 触发响应式更新 this.messageList = [...this.messageList];
this.scrollToBottom(); this.scrollToBottom();
} }
}, },
getPollInterval(attempt) {
if (attempt <= 10) return 500;
if (attempt <= 30) return 1000;
return 1500;
},
async pollChatStatus(conversationId, chatId, aiMsg) { async pollChatStatus(conversationId, chatId, aiMsg) {
const maxAttempts = 60; // 最多轮询60次每次1.5秒即90秒 const maxAttempts = 80;
let attempts = 0; let attempts = 0;
const checkStatus = async () => { const checkStatus = async () => {
@@ -761,37 +861,33 @@ export default {
} }
try { try {
const res = await api.cozeRetrieveChat({ const res = await api.cozeRetrieveChat({
conversationId, conversationId,
chatId chatId
}); });
const retrieveData = this.unwrapCozeResponse(res); const retrieveData = this.unwrapCozeResponse(res);
if (retrieveData) { if (retrieveData) {
console.log("====api.cozeRetrieveChat response====", retrieveData); const chatObj = retrieveData.chat || retrieveData;
const chatObj = retrieveData.chat || retrieveData; const status = chatObj && chatObj.status;
const status = chatObj && chatObj.status;
if (status === 'completed') {
if (status === 'completed') { await this.getChatMessages(conversationId, chatId, aiMsg);
// 对话完成,获取消息详情 } else if (status === 'failed' || status === 'canceled') {
await this.getChatMessages(conversationId, chatId, aiMsg); this.isLoading = false;
} else if (status === 'failed' || status === 'canceled') { const failMsg = `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}`;
this.isLoading = false; if (aiMsg) { aiMsg.content = failMsg; aiMsg.loading = false; this.messageList = [...this.messageList]; }
const failMsg = `抱歉,对话${status === 'canceled' ? '已取消' : '失败'}`; else { this.messageList.push({ role: 'ai', content: failMsg }); }
if (aiMsg) { aiMsg.content = failMsg; aiMsg.loading = false; this.messageList = [...this.messageList]; } this.scrollToBottom();
else { this.messageList.push({ role: 'ai', content: failMsg }); } } else {
this.scrollToBottom(); setTimeout(checkStatus, this.getPollInterval(attempts));
}
} else { } else {
// 继续轮询 (created, in_progress) 每 1.5 秒 setTimeout(checkStatus, this.getPollInterval(attempts));
setTimeout(checkStatus, 1500);
}
} else {
// 查询失败,重试
setTimeout(checkStatus, 1000);
} }
} catch (e) { } catch (e) {
console.error('查询对话状态失败:', 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 { .typing-indicator {
display: flex; display: flex;

View File

@@ -119,9 +119,9 @@
<text>{{ item.safety || '—' }}</text> <text>{{ item.safety || '—' }}</text>
</view> </view>
</view> </view>
<view v-if="item.category" class="category-badge"> <!-- <view v-if="item.category" class="category-badge">
<text>{{ item.category }}</text> <text>{{ item.category }}</text>
</view> </view> -->
</view> </view>
<view class="nutrition-list"> <view class="nutrition-list">
<view <view