Files
msh-system/msh_single_uniapp/api/tool.js
scottpan d8d2025543 feat: T10 回归测试 Bug 修复与功能完善
修复 BUG-001 至 BUG-009 及 T10-1 至 T10-6 相关问题:
- 打卡积分显示与累加逻辑优化
- 食谱计算器 Tab 选中样式修复
- 食物百科列表图片与简介展示修复
- 食物详情页数据加载修复
- AI营养师差异化回复优化
- 健康知识/营养知识名称统一
- 饮食指南/科普文章详情页内容展示修复
- 帖子营养统计数据展示修复
- 社区帖子类型中文命名统一
- 帖子详情标签中文显示修复
- 食谱营养AI填充功能完善
- 食谱收藏/点赞功能修复

新增:
- ToolNutritionFillService 营养填充服务
- T10 回归测试用例 (Playwright)
- 知识文章数据 SQL 脚本

涉及模块:
- crmeb-common: VO/Request/Response 优化
- crmeb-service: 业务逻辑完善
- crmeb-front: API 接口扩展
- msh_single_uniapp: 前端页面修复
- tests/e2e: 回归测试用例
2026-03-05 09:35:00 +08:00

565 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// +----------------------------------------------------------------------
// | Tool模块API接口
// +----------------------------------------------------------------------
// | 包含食谱计算器、AI营养师、饮食打卡、食物百科、营养知识等接口
// +----------------------------------------------------------------------
import request from "@/utils/request.js";
import { HTTP_REQUEST_URL, TOKENNAME } from "@/config/app.js";
import store from "@/store";
// ==================== 食谱计算器相关 ====================
/**
* 计算营养方案
* @param {Object} data - 计算参数
* @param {String} data.gender - 性别male/female
* @param {Number} data.age - 年龄
* @param {Number} data.height - 身高(cm)
* @param {Boolean} data.dialysis - 是否透析true/false
* @param {String} data.dialysisType - 透析类型hemodialysis/peritoneal (可选)
* @param {Number} data.dryWeight - 干体重(kg)
* @param {Number} data.creatinine - 血肌酐(μmol/L)
*/
export function calculateNutrition(data) {
return request.post('tool/calculator/calculate', data);
}
/**
* 获取计算结果详情
* @param {Number} id - 计算结果ID
*/
export function getCalculatorResult(id) {
return request.get('tool/calculator/result/' + id);
}
/**
* 采纳营养计划
* @param {Number} resultId - 计算结果ID
*/
export function adoptNutritionPlan(resultId) {
return request.post('tool/calculator/adopt', { resultId });
}
// ==================== AI营养师相关 ====================
/**
* 发送消息给AI营养师
* @param {Object} data - 消息数据
* @param {String} data.content - 消息内容
* @param {String} data.type - 消息类型text/image/voice
* @param {String} data.imageUrl - 图片URL当type为image时
* @param {String} data.voiceUrl - 语音URL当type为voice时
* @param {String} data.conversationId - 会话ID可选用于继续对话
*/
export function sendAIMessage(data) {
return request.post('tool/ai-nutritionist/message', data);
}
/**
* 获取AI回复
* @param {String} messageId - 消息ID
*/
export function getAIResponse(messageId) {
return request.get('tool/ai-nutritionist/response/' + messageId);
}
/**
* 获取对话历史
* @param {Object} data - 查询参数
* @param {String} data.conversationId - 会话ID可选
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function getConversationHistory(data) {
return request.get('tool/ai-nutritionist/history', data);
}
/**
* 清空对话历史
* @param {String} conversationId - 会话ID可选
*/
export function clearConversation(conversationId) {
return request.post('tool/ai-nutritionist/clear', { conversationId });
}
// ==================== 饮食打卡相关 ====================
/**
* 提交打卡记录
* @param {Object} data - 打卡数据
* @param {String} data.mealType - 餐次breakfast/lunch/dinner/snack
* @param {String} data.date - 打卡日期YYYY-MM-DD
* @param {Array} data.images - 图片URL数组
* @param {String} data.remark - 备注
* @param {String} data.voiceUrl - 语音备注URL可选
* @param {String} data.taskId - 语音识别任务ID可选
* @param {String} data.dishes - 菜品清单JSON字符串
* @param {Object} data.nutrition - 营养数据(可选)
* @param {Boolean} data.enableAIVideo - 是否生成AI视频
* @param {Boolean} data.enableAIAnalysis - 是否开启AI识别
*/
export function submitCheckin(data) {
return request.post('tool/checkin/submit', data);
}
/**
* 获取打卡记录列表
* @param {Object} data - 查询参数
* @param {String} data.date - 日期YYYY-MM-DD可选
* @param {String} data.mealType - 餐次(可选)
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function getCheckinList(data) {
return request.get('tool/checkin/list', data);
}
/**
* 获取打卡记录详情
* @param {Number} id - 打卡记录ID
*/
export function getCheckinDetail(id) {
return request.get('tool/checkin/detail/' + id);
}
/**
* 获取连续打卡统计
*/
export function getCheckinStreak() {
return request.get('tool/checkin/streak');
}
/**
* 获取打卡日历数据
* @param {String} yearMonth - 年月YYYY-MM
*/
export function getCheckinCalendar(yearMonth) {
return request.get('tool/checkin/calendar', { yearMonth });
}
/**
* 获取打卡任务列表
*/
export function getCheckinTasks() {
return request.get('tool/checkin/tasks');
}
/**
* 查询视频任务状态
* @param {String} taskId - 任务ID
*/
export function getVideoTaskStatus(taskId) {
return request.get(`kieai/video/task/${taskId}`);
}
/**
* 一键复制打卡
* @param {Number} sourceRecordId - 源打卡记录ID
* @param {Object} data - 修改后的数据(可选)
*/
export function copyCheckin(sourceRecordId, data) {
return request.post('tool/checkin/copy', {
sourceRecordId,
...data
});
}
/**
* 一键借鉴打卡
* @param {Number} sourcePostId - 源社区内容ID
* @param {Object} data - 修改后的数据(可选)
*/
export function learnCheckin(sourcePostId, data) {
return request.post('tool/checkin/learn', {
sourcePostId,
...data
});
}
// ==================== 食物百科相关 ====================
/**
* 搜索食物
* @param {Object} data - 搜索参数
* @param {String} data.keyword - 搜索关键词
* @param {String} data.category - 分类(可选)
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function searchFood(data) {
return request.get('tool/food/search', data);
}
/**
* 获取食物列表(按分类)
* @param {Object} data - 查询参数
* @param {String} data.category - 分类all/grain/vegetable/fruit/meat/seafood
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function getFoodList(data) {
return request.get('tool/food/list', data);
}
/**
* 获取食物详情(后端仅接受 Long 类型 id传 name 会 400
* @param {Number|String} id - 食物ID必须为数字不能传名称
*/
export function getFoodDetail(id) {
const numId = typeof id === 'number' && !isNaN(id) ? id : parseInt(String(id), 10);
// 打印请求参数便于确认(后端仅接受 Long 类型 id传 name 会 400
console.log('[api/tool] getFoodDetail 请求参数:', { id, numId, type: typeof id });
if (isNaN(numId)) {
return Promise.reject(new Error('食物详情接口需要数字ID当前传入: ' + id));
}
return request.get('tool/food/detail/' + numId);
}
/**
* 获取相似食物推荐
* @param {String} foodId - 食物ID
*/
export function getSimilarFoods(foodId) {
return request.get('tool/food/similar/' + foodId);
}
// ==================== 营养知识相关 ====================
/**
* 获取营养知识列表
* @param {Object} data - 查询参数
* @param {String} data.type - 类型nutrients/guide/article
* @param {String} data.category - 分类(可选)
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function getKnowledgeList(data) {
return request.get('tool/knowledge/list', data);
}
/**
* 获取营养知识详情
* @param {Number} id - 知识ID
*/
export function getKnowledgeDetail(id) {
return request.get('tool/knowledge/detail/' + id);
}
/**
* 获取营养素详情
* @param {String} name - 营养素名称
*/
export function getNutrientDetail(name) {
return request.get('tool/knowledge/nutrient/' + name);
}
/**
* AI 营养估算根据饮食描述文本返回估算的热量、蛋白质、钾、磷T06-T09
* @param {String} text - 饮食描述
* @returns {Promise<{data: {energyKcal?, proteinG?, potassiumMg?, phosphorusMg?}}>}
*/
export function getAiNutritionFill(text) {
return request.post('tool/nutrition/fill-ai', { text: text || '' });
}
// ==================== 打卡社区相关 ====================
/**
* 获取社区内容列表
* @param {Object} data - 查询参数
* @param {String} data.tab - Tab类型recommend/latest/follow/hot
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function getCommunityList(data) {
return request.get('tool/community/list', data);
}
/**
* 获取社区内容详情
* @param {Number} id - 内容ID
*/
export function getCommunityDetail(id) {
return request.get('tool/community/detail/' + id);
}
/**
* 发布社区内容
* @param {Object} data - 内容数据
* @param {String} data.title - 标题
* @param {String} data.content - 内容
* @param {Array} data.images - 图片URL数组
* @param {Array} data.tags - 标签数组
* @param {Number} data.checkInRecordId - 关联的打卡记录ID可选
*/
export function publishCommunityPost(data) {
return request.post('tool/community/publish', data);
}
/**
* 点赞/取消点赞
* @param {Number} postId - 内容ID会被转为数字以匹配后端 Long
* @param {Boolean} isLike - 是否点赞true/false
*/
export function toggleLike(postId, isLike) {
const id = typeof postId === 'number' && !isNaN(postId) ? postId : parseInt(postId, 10);
return request.post('tool/community/like', { postId: id, isLike: !!isLike });
}
/**
* 收藏/取消收藏
* @param {Number} postId - 内容ID会被转为数字以匹配后端 Long
* @param {Boolean} isCollect - 是否收藏true/false
*/
export function toggleCollect(postId, isCollect) {
const id = typeof postId === 'number' && !isNaN(postId) ? postId : parseInt(postId, 10);
return request.post('tool/community/collect', { postId: id, isCollect: !!isCollect });
}
/**
* 发表评论
* @param {Object} data - 评论数据
* @param {Number} data.postId - 内容ID
* @param {String} data.content - 评论内容
* @param {Number} data.parentCommentId - 父评论ID可选用于回复
* @param {Number} data.replyToUserId - 回复的用户ID可选
*/
export function addComment(data) {
return request.post('tool/community/comment', data);
}
/**
* 获取评论列表
* @param {Number} postId - 内容ID
* @param {Object} data - 查询参数
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function getCommentList(postId, data) {
return request.get('tool/community/comment/list/' + postId, data);
}
/**
* 关注/取消关注用户
* @param {Number} userId - 用户ID
* @param {Boolean} isFollow - 是否关注true/false
*/
export function toggleFollow(userId, isFollow) {
return request.post('tool/community/follow', { userId, isFollow });
}
/**
* 分享内容
* @param {Number} postId - 内容ID
*/
export function sharePost(postId) {
return request.post('tool/community/share', { postId });
}
/**
* 填充帖子营养数据(服务端根据帖子内容/打卡补充营养并回写)
* @param {Number|String} postId - 帖子ID
* @returns {Promise<{data?: object}>} 返回更新后的帖子或营养数据
*/
export function fillPostNutrition(postId) {
const id = typeof postId === 'number' && !isNaN(postId) ? postId : parseInt(postId, 10);
return request.post('tool/community/post/' + id + '/fill-nutrition');
}
// ==================== 积分系统相关 ====================
/**
* 获取用户积分信息
*/
export function getUserPoints() {
return request.get('tool/points/info');
}
/**
* 获取积分规则
*/
export function getPointsRules() {
return request.get('tool/points/rules');
}
/**
* 获取积分流水
* @param {Object} data - 查询参数
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
* @param {String} data.type - 类型earn/consume可选
*/
export function getPointsHistory(data) {
return request.get('tool/points/history', data);
}
/**
* 获取积分兑换列表
*/
export function getPointsExchangeList() {
return request.get('tool/points/exchange/list');
}
/**
* 积分兑换
* @param {Number} exchangeId - 兑换项ID
*/
export function exchangePoints(exchangeId) {
return request.post('tool/points/exchange', { exchangeId });
}
// ==================== 首页数据相关 ====================
/**
* 获取首页数据
*/
export function getHomeData() {
return request.get('tool/home/data');
}
/**
* 获取推荐食谱列表(首页展示,无需登录即可浏览)
* @param {Object} data - 查询参数
* @param {Number} data.limit - 数量限制
*/
export function getRecommendedRecipes(data) {
return request.get('tool/home/recipes', data, { noAuth: true });
}
/**
* 获取推荐营养知识(首页展示,无需登录即可浏览)
* @param {Object} data - 查询参数
* @param {Number} data.limit - 数量限制
*/
export function getRecommendedKnowledge(data) {
return request.get('tool/home/knowledge', data, { noAuth: true });
}
/**
* 获取用户健康档案状态(首页展示,未登录时返回默认状态)
*/
export function getUserHealthStatus() {
return request.get('tool/home/health-status', {}, { noAuth: true });
}
/**
* 获取首页展示配置(如四大功能入口是否显示,由系统配置 field01 控制1=显示)
*/
export function getHomeDisplayConfig() {
return request.get('tool/home/display-config', {}, { noAuth: true });
}
// ==================== 食谱相关 ====================
/**
* 获取食谱列表
* @param {Object} data - 查询参数
* @param {String} data.mealType - 餐次(可选)
* @param {Number} data.page - 页码
* @param {Number} data.limit - 每页数量
*/
export function getRecipeList(data) {
return request.get('tool/recipe/list', data);
}
/**
* 获取食谱详情
* @param {Number} id - 食谱ID
*/
export function getRecipeDetail(id) {
return request.get('tool/recipe/detail/' + id);
}
/**
* 收藏/取消收藏食谱
* @param {Number} recipeId - 食谱ID
* @param {Boolean} isFavorite - 是否收藏true/false
*/
export function toggleRecipeFavorite(recipeId, isFavorite) {
return request.post('tool/recipe/favorite', { recipeId, isFavorite });
}
// T09: 食谱营养 AI 回填
export function fillRecipeNutrition(recipeId) {
return request.post("tool/recipe/" + recipeId + "/fill-nutrition")
}
// ==================== 文件上传相关 ====================
/**
* 上传图片
* @param {String} filePath - 图片临时路径
* @param {String} type - 上传类型checkin/community/avatar
*/
export function uploadImage(filePath, type = 'checkin') {
return new Promise((resolve, reject) => {
const token = store.state.app.token || '';
const header = {};
if (token) {
header[TOKENNAME] = token;
}
uni.uploadFile({
url: HTTP_REQUEST_URL + '/api/front/tool/upload/image',
filePath: filePath,
name: 'file',
formData: {
type: type
},
header: header,
success: (res) => {
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
if (data.code === 200) {
resolve(data.data);
} else {
reject(data.message || '上传失败');
}
} catch (e) {
reject('解析响应失败');
}
},
fail: (err) => {
reject('上传失败:' + (err.errMsg || '网络错误'));
}
});
});
}
/**
* 上传语音
* @param {String} filePath - 语音临时路径
*/
export function uploadVoice(filePath) {
return new Promise((resolve, reject) => {
const token = store.state.app.token || '';
const header = {};
if (token) {
header[TOKENNAME] = token;
}
uni.uploadFile({
url: HTTP_REQUEST_URL + '/api/front/tool/upload/voice',
filePath: filePath,
name: 'file',
header: header,
success: (res) => {
try {
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
if (data.code === 200) {
resolve(data.data);
} else {
reject(data.message || '上传失败');
}
} catch (e) {
reject('解析响应失败');
}
},
fail: (err) => {
reject('上传失败:' + (err.errMsg || '网络错误'));
}
});
});
}