fix: 修复手动测试发现的3项问题

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 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-25 14:54:31 +08:00
parent ba08abd374
commit 355895dba2
3 changed files with 71 additions and 53 deletions

View File

@@ -96,6 +96,8 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService {
lqw.eq(V2Knowledge::getType, "nutrients"); lqw.eq(V2Knowledge::getType, "nutrients");
lqw.eq(V2Knowledge::getNutrientName, name); lqw.eq(V2Knowledge::getNutrientName, name);
lqw.eq(V2Knowledge::getStatus, "published"); lqw.eq(V2Knowledge::getStatus, "published");
lqw.orderByDesc(V2Knowledge::getKnowledgeId);
lqw.last("LIMIT 1");
V2Knowledge knowledge = v2KnowledgeDao.selectOne(lqw); V2Knowledge knowledge = v2KnowledgeDao.selectOne(lqw);
if (knowledge == null) { if (knowledge == null) {

View File

@@ -629,57 +629,59 @@ export default {
async sendToAI(content, type) { async sendToAI(content, type) {
this.isLoading = true; this.isLoading = true;
// BUG-005文本/多模态必须走 KieAI Gemini请求体 { messages: [{ role: 'user', content: 用户输入 }], stream: false } // 统一走 Coze API文本、多模态、图片均使用 Coze Bot
// 回复仅从 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 路径,保留兼容)
const userId = this.uid || (uni.getStorageSync('userInfo') || {}).id || 'default_user'; const userId = this.uid || (uni.getStorageSync('userInfo') || {}).id || 'default_user';
try { try {
const messages = []; const messages = [];
let fileInfo = content; if (type === 'text') {
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({ messages.push({
role: 'user', role: 'user',
content: JSON.stringify([{ type: 'image', file_id: fileId }]), content: typeof content === 'string' ? content : JSON.stringify(content),
content_type: 'object_string'
});
} else {
messages.push({
role: 'user',
content: '我发送了一张图片,请帮我分析',
content_type: 'text' 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 = { const requestData = {
botId: this.botId, botId: this.botId,

View File

@@ -99,7 +99,7 @@
<view <view
class="food-item" class="food-item"
v-for="(item, index) in filteredFoodList" v-for="(item, index) in filteredFoodList"
:key="item.id != null ? item.id : index" :key="index"
@click="goToFoodDetail(item)" @click="goToFoodDetail(item)"
> >
<view class="food-image-wrapper"> <view class="food-image-wrapper">
@@ -244,7 +244,10 @@ export default {
}, },
computed: { computed: {
filteredFoodList() { 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) { onLoad(options) {
@@ -264,7 +267,7 @@ export default {
}); });
const rawList = this.getRawFoodList(result); const rawList = this.getRawFoodList(result);
this.imageErrorIds = {}; this.imageErrorIds = {};
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)); this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)).filter(i => i != null);
} catch (error) { } catch (error) {
console.error('加载食物列表失败:', error); console.error('加载食物列表失败:', error);
} }
@@ -319,6 +322,13 @@ export default {
} }
}, },
normalizeFoodItem(item) { normalizeFoodItem(item) {
if (!item) return null;
// 英文分类 key → 中文显示名称
const categoryNameMap = {
grain: '谷薯类', vegetable: '蔬菜类', fruit: '水果类',
meat: '肉蛋类', seafood: '水产类', dairy: '奶类',
bean: '豆类', nut: '坚果类', all: '全部'
};
const safetyMap = { const safetyMap = {
suitable: { safety: '放心吃', safetyClass: 'safe' }, suitable: { safety: '放心吃', safetyClass: 'safe' },
moderate: { safety: '限量吃', safetyClass: 'limited' }, moderate: { safety: '限量吃', safetyClass: 'limited' },
@@ -368,12 +378,16 @@ export default {
? (typeof rawId === 'number' ? rawId : Number(rawId)) ? (typeof rawId === 'number' ? rawId : Number(rawId))
: undefined; : undefined;
// 保证列表项必有 image/imageUrl空时由 getFoodImage 用 defaultPlaceholder和 nutrition 数组(.nutrition-item 数据来源) // 保证列表项必有 image/imageUrl空时由 getFoodImage 用 defaultPlaceholder和 nutrition 数组(.nutrition-item 数据来源)
// 将英文分类名映射为中文,若已是中文则保留
const rawCategory = item.category || item.categoryName || item.category_name || '';
const category = categoryNameMap[rawCategory] || rawCategory;
return { return {
...item, ...item,
id: numericId, id: numericId,
image: image || '', image: image || '',
imageUrl: image || '', imageUrl: image || '',
category: item.category || '', category,
safety: safety.safety, safety: safety.safety,
safetyClass: safety.safetyClass, safetyClass: safety.safetyClass,
nutrition: Array.isArray(nutrition) ? nutrition : [] nutrition: Array.isArray(nutrition) ? nutrition : []
@@ -404,7 +418,7 @@ export default {
}); });
const rawList = this.getRawFoodList(result); const rawList = this.getRawFoodList(result);
this.imageErrorIds = {}; this.imageErrorIds = {};
this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)); this.foodList = (rawList || []).map(item => this.normalizeFoodItem(item)).filter(i => i != null);
} catch (error) { } catch (error) {
console.error('搜索失败:', error); console.error('搜索失败:', error);
} }