323 lines
11 KiB
Markdown
323 lines
11 KiB
Markdown
|
|
# 测试问题与优化建议 — 修改解决方案
|
|||
|
|
|
|||
|
|
**分析日期:** 2026年4月11日
|
|||
|
|
**项目:** msh_single_uniapp(UniApp 小程序)
|
|||
|
|
**涉及页面:** calculator-result / ai-nutritionist / food-encyclopedia
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、总览:需处理项 vs 暂不处理项
|
|||
|
|
|
|||
|
|
| # | 问题 | 决策 | 涉及文件 |
|
|||
|
|
|---|------|------|----------|
|
|||
|
|
| 1 | 食物份数 → 克数 | **采纳** | `pages/tool/calculator-result.vue` |
|
|||
|
|
| 2 | AI对话交互按钮 | **部分采纳** | `pages/tool/ai-nutritionist.vue` |
|
|||
|
|
| 3 | AI回复速度慢 | **已优化(切换Coze流式API)** | `ai-nutritionist.vue` + `models-api.js` |
|
|||
|
|
| 4 | 食物百科图片缺失 | **不处理**(后台可改) | — |
|
|||
|
|
| 5 | 百科卡片误导点击 | **采纳**(跳转详情页) | `pages/tool/food-encyclopedia.vue` |
|
|||
|
|
| 6 | 角色权限与分销 | **暂不处理**(后台人工配置) | — |
|
|||
|
|
| 7 | 百科点击报错 Bug | **必须修复** | `pages/tool/food-encyclopedia.vue` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、需修改的问题及代码级解决方案
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 问题1:食物份数建议改为克数
|
|||
|
|
|
|||
|
|
**页面:** `pages/tool/calculator-result.vue`
|
|||
|
|
|
|||
|
|
**现状分析:**
|
|||
|
|
当前"食物份数建议"卡片(约第94-113行)中,食物列表渲染逻辑为:
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<text class="food-portion">{{ item.portion }} 份</text>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
数据结构 `foodList` 中每个 item 包含 `{ number, name, portion }`,`portion` 为份数值。
|
|||
|
|
|
|||
|
|
**修改方案:**
|
|||
|
|
|
|||
|
|
1. **后端接口改造**(推荐):`getCalculatorResult` 接口返回数据中增加 `gram` 字段,或将 `portion` 的含义改为克数。
|
|||
|
|
|
|||
|
|
2. **前端模板修改**(`calculator-result.vue` 约第94-113行):
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<!-- 修改前 -->
|
|||
|
|
<text class="card-title">食物份数建议</text>
|
|||
|
|
...
|
|||
|
|
<text class="food-portion">{{ item.portion }} 份</text>
|
|||
|
|
|
|||
|
|
<!-- 修改后 -->
|
|||
|
|
<text class="card-title">每日食物建议</text>
|
|||
|
|
...
|
|||
|
|
<text class="food-portion">{{ item.gram || item.portion }} 克</text>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **数据层适配**(`applyResult` 方法,约第320-350行):解析接口返回时,确保 `foodList` 中的 item 携带 `gram` 字段。如后端暂未改造,可前端用换算公式临时过渡:
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 在 applyResult 方法中
|
|||
|
|
this.foodList = (res.data.foodList || []).map(item => ({
|
|||
|
|
...item,
|
|||
|
|
gram: item.gram || Math.round(item.portion * item.gramPerServing) || item.portion
|
|||
|
|
}))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**工作量估计:** 前端 0.5天,后端(如需改接口)0.5天
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 问题2:AI营养师对话页面新增交互按钮
|
|||
|
|
|
|||
|
|
**页面:** `pages/tool/ai-nutritionist.vue`
|
|||
|
|
|
|||
|
|
**现状分析:**
|
|||
|
|
当前每条 AI 消息仅有 **1个操作按钮**:TTS 语音朗读(约第84-90行)。消息数据结构为 `{ role, content, type, loading, streaming }`。AI 回复通过 `api.kieaiGeminiChat()` 调用(实际走 `/api/front/kieai/gemini/chat`,`stream: false` 非流式),整体响应一次性返回。
|
|||
|
|
|
|||
|
|
**部分采纳后的需求:** 新增"复制"、"重新生成"、"删除"按钮(语音朗读已有)。
|
|||
|
|
|
|||
|
|
**修改方案:**
|
|||
|
|
|
|||
|
|
在消息气泡下方的操作区域(约第84-90行 TTS 按钮位置),扩展为按钮组:
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<!-- 修改前:仅 TTS 按钮 -->
|
|||
|
|
<view class="tts-play-btn" v-if="msg.role === 'ai' && !msg.loading && !msg.streaming && msg.content && msg.type !== 'image'" @click="...">
|
|||
|
|
<text>{{ ttsPlayingIndex === index ? '⏹' : '▶' }}</text>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 修改后:操作按钮组 -->
|
|||
|
|
<view class="msg-actions" v-if="msg.role === 'ai' && !msg.loading && !msg.streaming && msg.content && msg.type !== 'image'">
|
|||
|
|
<!-- 复制 -->
|
|||
|
|
<view class="action-btn" @click="copyMessage(index)">
|
|||
|
|
<text class="action-icon">📋</text>
|
|||
|
|
</view>
|
|||
|
|
<!-- 重新生成(仅最后一条AI消息显示) -->
|
|||
|
|
<view class="action-btn" v-if="isLastAiMessage(index)" @click="regenerateMessage(index)">
|
|||
|
|
<text class="action-icon">🔄</text>
|
|||
|
|
</view>
|
|||
|
|
<!-- 语音朗读(已有) -->
|
|||
|
|
<view class="action-btn" @click="ttsPlayingIndex === index ? stopTTS() : playTTS(index)">
|
|||
|
|
<text class="action-icon">{{ ttsPlayingIndex === index ? '⏹' : '▶' }}</text>
|
|||
|
|
</view>
|
|||
|
|
<!-- 删除 -->
|
|||
|
|
<view class="action-btn" @click="deleteMessage(index)">
|
|||
|
|
<text class="action-icon">🗑️</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**新增 methods:**
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 复制消息
|
|||
|
|
copyMessage(index) {
|
|||
|
|
const msg = this.messageList[index]
|
|||
|
|
uni.setClipboardData({
|
|||
|
|
data: msg.content,
|
|||
|
|
success: () => uni.showToast({ title: '已复制', icon: 'success' })
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 删除单条消息
|
|||
|
|
deleteMessage(index) {
|
|||
|
|
uni.showModal({
|
|||
|
|
title: '提示',
|
|||
|
|
content: '确定删除这条消息吗?',
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.confirm) {
|
|||
|
|
this.messageList.splice(index, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 重新生成(重发上一条用户消息)
|
|||
|
|
regenerateMessage(index) {
|
|||
|
|
// 找到该AI消息对应的上一条用户消息
|
|||
|
|
let userMsgIndex = index - 1
|
|||
|
|
while (userMsgIndex >= 0 && this.messageList[userMsgIndex].role !== 'user') {
|
|||
|
|
userMsgIndex--
|
|||
|
|
}
|
|||
|
|
if (userMsgIndex < 0) return
|
|||
|
|
// 移除当前AI回复
|
|||
|
|
this.messageList.splice(index, 1)
|
|||
|
|
// 重新发送
|
|||
|
|
this.sendToAI()
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 判断是否为最后一条AI消息
|
|||
|
|
isLastAiMessage(index) {
|
|||
|
|
for (let i = this.messageList.length - 1; i >= 0; i--) {
|
|||
|
|
if (this.messageList[i].role === 'ai') return i === index
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**新增样式:**
|
|||
|
|
|
|||
|
|
```scss
|
|||
|
|
.msg-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 16rpx;
|
|||
|
|
margin-top: 8rpx;
|
|||
|
|
justify-content: flex-start;
|
|||
|
|
}
|
|||
|
|
.action-btn {
|
|||
|
|
width: 56rpx;
|
|||
|
|
height: 56rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #f4f5f7;
|
|||
|
|
}
|
|||
|
|
.action-icon {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**工作量估计:** 1天
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 问题3:AI回复速度慢(已优化 — 切换至 Coze API)
|
|||
|
|
|
|||
|
|
**优化前调用链路:**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
小程序 → POST /api/front/kieai/gemini/chat/stream
|
|||
|
|
→ Spring KieAIController (SseEmitter)
|
|||
|
|
→ OkHttp3 异步 POST https://api.kie.ai/gemini-2.5-flash/v1/chat/completions
|
|||
|
|
→ KieAI 代理网关 → Gemini 2.5 Flash 模型
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优化后调用链路:**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
小程序 → POST /api/front/coze/chat/stream
|
|||
|
|
→ Spring CozeController (SseEmitter, 120s timeout, 15s heartbeat)
|
|||
|
|
→ Coze 官方 SDK (client.chat().stream())
|
|||
|
|
→ https://api.coze.cn → Coze Bot (7591133240535449654)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**已完成的优化:**
|
|||
|
|
|
|||
|
|
1. **前端切换至 Coze 流式 API:** `sendToAI()` 改为调用 `api.cozeChatStream()`,直接使用 Coze 的流式事件(`conversation.message.delta`),逐字渲染 AI 回复。首字可见时间大幅缩短。
|
|||
|
|
|
|||
|
|
2. **Coze 事件解析:** 正确解析 Coze SSE 事件格式 `{ event, conversation_id, chat_id, content, role, type }`,仅累积 `event=conversation.message.delta` 且 `type=answer` 的文本增量。
|
|||
|
|
|
|||
|
|
3. **多轮对话支持:** 自动保存 `conversation_id`,后续消息在同一会话中进行,Coze Bot 可获取完整上下文。
|
|||
|
|
|
|||
|
|
4. **消息格式适配:** 新增 `buildCozeMessages()` / `buildCozeRequest()` 方法,将用户输入(文本/图片/多模态)转换为 Coze 格式的 `additionalMessages`。
|
|||
|
|
|
|||
|
|
5. **降级策略:** 流式失败时自动降级为非流式 `api.cozeChat()`,并通过 `pollCozeResult()` 轮询获取最终回复。
|
|||
|
|
|
|||
|
|
**Coze 相比 KieAI 代理的优势:**
|
|||
|
|
|
|||
|
|
- 去掉一层 KieAI 代理网关,减少一跳网络延迟
|
|||
|
|
- Coze 后端已配置 `X-Accel-Buffering: no` + `Cache-Control: no-cache`,SSE 不会被 nginx 缓冲
|
|||
|
|
- Coze SDK 内置连接池管理,不再每次请求 new OkHttpClient
|
|||
|
|
- 内置 15s heartbeat 防止连接超时断开
|
|||
|
|
- 支持通过 `conversation_id` 进行多轮上下文对话
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 问题4:食物百科图片缺失
|
|||
|
|
|
|||
|
|
**决策:不处理。** 图片数据通过商城 PC 管理后台维护,属运营侧工作。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 问题5:百科卡片点击 → 跳转食物详情页
|
|||
|
|
|
|||
|
|
**页面:** `pages/tool/food-encyclopedia.vue`
|
|||
|
|
|
|||
|
|
**现状分析:**
|
|||
|
|
当前卡片已绑定了 `@click="goToFoodDetail(item)"`(约第103行),`goToFoodDetail` 方法(约第1012-1030行)会尝试提取 item 的 id 并跳转到 `/pages/tool/food-detail`。项目中已存在 `pages/tool/food-detail.vue` 页面。
|
|||
|
|
|
|||
|
|
**问题:** 跳转逻辑本身已实现,但存在 Bug(见问题7),导致点击报错而非正常跳转,用户看到的效果就是"点击无反应"或"点击后闪退"。
|
|||
|
|
|
|||
|
|
**修改方案:** 修复问题7的 Bug 后,卡片点击跳转详情页的功能即可正常工作。需确认:
|
|||
|
|
|
|||
|
|
1. `food-detail.vue` 详情页内容是否完整可用
|
|||
|
|
2. `pages.json` 中是否已注册 `/pages/tool/food-detail` 路由
|
|||
|
|
|
|||
|
|
**工作量估计:** 与问题7合并处理,0.5天
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 问题6:角色权限与分销系统
|
|||
|
|
|
|||
|
|
**决策:暂不处理。** 临时方案为运营人员在后台手动为指定用户账号配置角色标签(医生/护士/普通用户)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 问题7(Bug):食物百科点击报错 TypeError
|
|||
|
|
|
|||
|
|
**页面:** `pages/tool/food-encyclopedia.vue`
|
|||
|
|
**错误信息:**
|
|||
|
|
```
|
|||
|
|
TypeError: Cannot read property 'id' of undefined
|
|||
|
|
at pickNumericIdStr (food-encyclopedia.vue:1015)
|
|||
|
|
at VueComponent.goToFoodDetail (food-encyclopedia.vue:1022)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**根因分析:**
|
|||
|
|
|
|||
|
|
`goToFoodDetail(item)` 方法(第1012行)中,内部闭包 `pickNumericIdStr` 直接访问 `item.id`(第1015行),但未做空值校验。当 `filteredFoodList` 中的某个 item 为 `undefined` 或 `null` 时(可能因 API 返回脏数据、`normalizeFoodItem` 边界情况),点击即触发 TypeError。
|
|||
|
|
|
|||
|
|
**修复方案:**
|
|||
|
|
|
|||
|
|
在 `goToFoodDetail` 方法开头增加防御性校验(约第1012行):
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
goToFoodDetail(item) {
|
|||
|
|
// 防御性校验:避免 item 为空时报错
|
|||
|
|
if (!item || typeof item !== 'object') {
|
|||
|
|
console.warn('[food-encyclopedia] goToFoodDetail: item 为空,跳过跳转')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const pickNumericIdStr = () => {
|
|||
|
|
const cands = [item.id, item.foodId, item.food_id, item.v2FoodId, item.v2_food_id, item.foodID]
|
|||
|
|
// ... 原有逻辑不变
|
|||
|
|
}
|
|||
|
|
// ... 后续逻辑不变
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
同时建议加固 `filteredFoodList` 计算属性(约第261行),确保过滤掉无效项:
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
filteredFoodList() {
|
|||
|
|
const list = (this.foodList || []).filter(item => item != null && typeof item === 'object' && item.name)
|
|||
|
|
// ... 后续过滤逻辑不变
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**工作量估计:** 0.5天(含测试验证)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、执行优先级排序
|
|||
|
|
|
|||
|
|
| 优先级 | 问题 | 预估工时 | 负责方 |
|
|||
|
|
|--------|------|----------|--------|
|
|||
|
|
| **P0** | #7 百科点击报错 Bug | 0.5天 | 前端 |
|
|||
|
|
| **P1** | #1 份数→克数 | 0.5-1天 | 前端+后端 |
|
|||
|
|
| **P1** | #5 百科卡片跳转详情页 | 与#7合并 | 前端 |
|
|||
|
|
| **P2** | #3 AI响应速度(已切换Coze流式API) | ✅ 已完成 | 前端 |
|
|||
|
|
| **P2** | #2 AI对话新增操作按钮 | 1天 | 前端 |
|
|||
|
|
| **—** | #4 百科图片 | 不处理 | 运营 |
|
|||
|
|
| **—** | #6 角色权限 | 暂不处理 | 运营(后台手动) |
|
|||
|
|
|
|||
|
|
**总预估:** 前端约 3-4天,后端约 2-3天
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*基于 msh_single_uniapp 项目代码分析生成,所有代码引用均已核对源文件。*
|