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:
Submodule .claude/worktrees/hopeful-goldberg deleted from c69ce2891f
Submodule .claude/worktrees/suspicious-antonelli deleted from 6f2dc27fbc
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.DS_Store
|
||||||
|
.claude/worktrees/
|
||||||
14
README.md
14
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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 文档索引
|
## 文档索引
|
||||||
|
|
||||||
| 文档 | 路径 |
|
| 文档 | 路径 |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 #设置单个文件大小
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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分钟即可
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user