From 355895dba29210b77e9921ae7d29f0f564950ba2 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 25 Mar 2026 14:54:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=8F=91=E7=8E=B0=E7=9A=843=E9=A1=B9?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. food-encyclopedia: 修复 v-for key id:index TypeError - :key 改为 :key="index",避免 WeChat 小程序 key 表达式异常 - filteredFoodList 加本地搜索过滤 + null item 过滤 - normalizeFoodItem 新增英文→中文分类名映射(grain→谷薯类等) - loadFoodList/handleSearch 过滤 null 条目 2. ToolKnowledgeServiceImpl: 修复 TooManyResultsException - getNutrientDetail 查询新增 LIMIT 1 + ORDER BY knowledge_id DESC - 防止 DB 中同名营养素存在多条导致 selectOne 异常 3. ai-nutritionist: 统一走 Coze API,移除 KieAI Gemini 路径 - sendToAI 文本/多模态均改为 api.cozeChat + pollChatStatus - 支持 type='text'/'multimodal'/图片 三种消息类型构建 Co-Authored-By: Claude Sonnet 4.6 --- .../impl/tool/ToolKnowledgeServiceImpl.java | 4 +- .../pages/tool/ai-nutritionist.vue | 90 ++++++++++--------- .../pages/tool/food-encyclopedia.vue | 30 +++++-- 3 files changed, 71 insertions(+), 53 deletions(-) diff --git a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java index 14c41b7..c9491b0 100644 --- a/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java +++ b/msh_crmeb_22/crmeb-service/src/main/java/com/zbkj/service/service/impl/tool/ToolKnowledgeServiceImpl.java @@ -96,7 +96,9 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService { lqw.eq(V2Knowledge::getType, "nutrients"); lqw.eq(V2Knowledge::getNutrientName, name); lqw.eq(V2Knowledge::getStatus, "published"); - + lqw.orderByDesc(V2Knowledge::getKnowledgeId); + lqw.last("LIMIT 1"); + V2Knowledge knowledge = v2KnowledgeDao.selectOne(lqw); if (knowledge == null) { return new HashMap<>(); diff --git a/msh_single_uniapp/pages/tool/ai-nutritionist.vue b/msh_single_uniapp/pages/tool/ai-nutritionist.vue index 70b4955..6a9a534 100644 --- a/msh_single_uniapp/pages/tool/ai-nutritionist.vue +++ b/msh_single_uniapp/pages/tool/ai-nutritionist.vue @@ -629,57 +629,59 @@ export default { async sendToAI(content, type) { this.isLoading = true; - // BUG-005:文本/多模态必须走 KieAI Gemini;请求体 { messages: [{ role: 'user', content: 用户输入 }], stream: false } - // 回复仅从 data.choices[0].message.content 取,禁止使用 getAIResponse 等固定话术 - if (type === 'text' || type === 'multimodal') { - try { - const messages = [{ role: 'user', content: content }]; - const response = await api.kieaiGeminiChat({ messages, stream: false }); - this.isLoading = false; - if (response && response.code === 200 && response.data) { - const data = response.data; - const choice = data.choices && data.choices[0]; - const msgObj = choice && choice.message; - const rawContent = msgObj != null ? msgObj.content : undefined; - const reply = rawContent != null && rawContent !== undefined ? this.extractReplyContent(rawContent) : ''; - // BUG-005: 仅展示接口返回的 data.choices[0].message.content,不使用固定话术 - this.messageList.push({ role: 'ai', content: reply.trim() || '未能获取到有效回复。' }); - } else { - const msg = (response && response.message) || '发起对话失败'; - this.messageList.push({ role: 'ai', content: '请求失败:' + msg }); - } - this.scrollToBottom(); - } catch (error) { - console.error('KieAI 对话失败:', error); - this.isLoading = false; - const errMsg = (error && (error.message || error.msg)) || '请稍后再试'; - this.messageList.push({ role: 'ai', content: '请求失败:' + errMsg }); - this.scrollToBottom(); - } - return; - } - - // 仅图片且未合并(旧 Coze 路径,保留兼容) + // 统一走 Coze API(文本、多模态、图片均使用 Coze Bot) const userId = this.uid || (uni.getStorageSync('userInfo') || {}).id || 'default_user'; try { const messages = []; - 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) { + if (type === 'text') { + // 纯文字消息 messages.push({ role: 'user', - content: JSON.stringify([{ type: 'image', file_id: fileId }]), - content_type: 'object_string' - }); - } else { - messages.push({ - role: 'user', - content: '我发送了一张图片,请帮我分析', + 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, diff --git a/msh_single_uniapp/pages/tool/food-encyclopedia.vue b/msh_single_uniapp/pages/tool/food-encyclopedia.vue index 7f37e6e..d432d60 100644 --- a/msh_single_uniapp/pages/tool/food-encyclopedia.vue +++ b/msh_single_uniapp/pages/tool/food-encyclopedia.vue @@ -96,10 +96,10 @@ 共 {{ filteredFoodList.length }} 种食物 - @@ -244,7 +244,10 @@ export default { }, computed: { filteredFoodList() { - return this.foodList + const list = (this.foodList || []).filter(item => item != null); + if (!this.searchText || !this.searchText.trim()) return list; + const kw = this.searchText.trim().toLowerCase(); + return list.filter(item => item.name && item.name.toLowerCase().includes(kw)); }, }, onLoad(options) { @@ -264,7 +267,7 @@ export default { }); const rawList = this.getRawFoodList(result); this.imageErrorIds = {}; - this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)); + this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)).filter(i => i != null); } catch (error) { console.error('加载食物列表失败:', error); } @@ -319,6 +322,13 @@ export default { } }, normalizeFoodItem(item) { + if (!item) return null; + // 英文分类 key → 中文显示名称 + const categoryNameMap = { + grain: '谷薯类', vegetable: '蔬菜类', fruit: '水果类', + meat: '肉蛋类', seafood: '水产类', dairy: '奶类', + bean: '豆类', nut: '坚果类', all: '全部' + }; const safetyMap = { suitable: { safety: '放心吃', safetyClass: 'safe' }, moderate: { safety: '限量吃', safetyClass: 'limited' }, @@ -368,12 +378,16 @@ export default { ? (typeof rawId === 'number' ? rawId : Number(rawId)) : undefined; // 保证列表项必有 image/imageUrl(空时由 getFoodImage 用 defaultPlaceholder)和 nutrition 数组(.nutrition-item 数据来源) + // 将英文分类名映射为中文,若已是中文则保留 + const rawCategory = item.category || item.categoryName || item.category_name || ''; + const category = categoryNameMap[rawCategory] || rawCategory; + return { ...item, id: numericId, image: image || '', imageUrl: image || '', - category: item.category || '', + category, safety: safety.safety, safetyClass: safety.safetyClass, nutrition: Array.isArray(nutrition) ? nutrition : [] @@ -404,7 +418,7 @@ export default { }); const rawList = this.getRawFoodList(result); this.imageErrorIds = {}; - this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)); + this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)).filter(i => i != null); } catch (error) { console.error('搜索失败:', error); }