feat: 营养素AI生成落库 + AI营养师消息级loading占位(设计文档对齐)

后端:
- ToolKnowledgeService/Impl 新增 generateNutrientContent()
  调用 Coze AI 批量生成6种营养素(蛋白质/钾/磷/钠/钙/水分)
  科普内容并写入 v2_knowledge,已存在的自动跳过
- ToolController 新增 POST /tool/knowledge/generate-nutrients
  端点(管理端一次性调用后自动补充封面图)
- 新增 SQL 备用脚本 migration_2026-03-25_nutrient_knowledge.sql
  含6种营养素完整JSON,直接执行可跳过AI生成

前端(ai-nutritionist.vue,对齐功能开发详细设计文档任务3-2):
- 新增 sleep(ms) 工具方法
- sendToAI 发起前先推入 {loading:true} 占位气泡
- pollChatStatus 轮询间隔由 1000ms 调整为 1500ms
- getChatMessages 回调填充占位气泡(不再 push 新消息)
- 所有错误/超时/失败路径统一更新 aiMsg.loading=false

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-25 15:32:53 +08:00
parent 24f75d198c
commit 6ec9487597
5 changed files with 269 additions and 55 deletions

View File

@@ -349,6 +349,17 @@ public class ToolController {
return CommonResult.success(updated);
}
/**
* 批量通过 Coze AI 生成 6 种营养素科普内容并写入 v2_knowledge管理端一次性调用
* 已存在的营养素自动跳过。调用前请确保 Coze PAT Token 有效。
*/
@ApiOperation(value = "AI生成营养素知识内容并落库")
@PostMapping("/knowledge/generate-nutrients")
public CommonResult<String> generateNutrientContent() {
int count = toolKnowledgeService.generateNutrientContent();
return CommonResult.success("成功生成 " + count + " 条营养素内容");
}
// ==================== AI 营养填充T06/T07 ====================
/**

View File

@@ -13,8 +13,13 @@ import com.zbkj.service.service.tool.ToolKnowledgeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.zbkj.common.request.coze.CozeChatRequest;
import com.zbkj.common.response.CozeBaseResponse;
import com.zbkj.service.service.tool.ToolCozeService;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -35,6 +40,9 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService {
@Resource
private DishImageService dishImageService;
@Resource
private ToolCozeService toolCozeService;
/**
* 获取营养知识列表
* @param pageParamRequest 分页参数
@@ -176,4 +184,136 @@ public class ToolKnowledgeServiceImpl implements ToolKnowledgeService {
stats.put("byStatus", byStatus);
return stats;
}
/**
* 批量通过 Coze AI 生成 6 种营养素科普内容并写入 v2_knowledge 表
* 已存在的营养素自动跳过,避免重复插入。
*/
@Override
public int generateNutrientContent() {
String[] nutrients = {"蛋白质", "", "", "", "", "水分"};
int success = 0;
for (int i = 0; i < nutrients.length; i++) {
String nutrient = nutrients[i];
try {
// 1. 检查是否已存在已发布的记录
LambdaQueryWrapper<V2Knowledge> existQuery = new LambdaQueryWrapper<>();
existQuery.eq(V2Knowledge::getType, "nutrients")
.eq(V2Knowledge::getNutrientName, nutrient)
.eq(V2Knowledge::getStatus, "published");
Long count = v2KnowledgeDao.selectCount(existQuery);
if (count > 0) {
log.info("[generateNutrient] 营养素 {} 已存在,跳过", nutrient);
continue;
}
// 2. 调用 Coze AI 生成内容
String prompt = buildNutrientPrompt(nutrient);
CozeChatRequest req = new CozeChatRequest();
req.setBotId("7591133240535449654");
req.setUserId("system_admin");
req.setStream(false);
CozeChatRequest.ChatMessage msg = new CozeChatRequest.ChatMessage();
msg.setRole("user");
msg.setContent(prompt);
msg.setContentType("text");
req.setAdditionalMessages(java.util.Collections.singletonList(msg));
CozeBaseResponse<Object> resp = toolCozeService.chat(req);
String content = extractCozeContent(resp);
if (StrUtil.isBlank(content) || content.startsWith("AI生成失败")) {
log.warn("[generateNutrient] {} 内容生成为空或失败,跳过", nutrient);
continue;
}
// 3. 写入 v2_knowledge 表
V2Knowledge knowledge = new V2Knowledge();
knowledge.setType("nutrients");
knowledge.setNutrientName(nutrient);
knowledge.setTitle(nutrient + " — CKD患者膳食管理");
knowledge.setContent(content);
knowledge.setSummary("了解" + nutrient + "在慢性肾病饮食中的重要性");
knowledge.setStatus("published");
knowledge.setSortOrder(i + 1);
knowledge.setViewCount(0);
knowledge.setLikeCount(0);
knowledge.setCreatedAt(new Date());
knowledge.setPublishedAt(new Date());
v2KnowledgeDao.insert(knowledge);
success++;
log.info("[generateNutrient] 营养素 {} 生成成功knowledge_id={}", nutrient, knowledge.getKnowledgeId());
} catch (Exception e) {
log.error("[generateNutrient] 营养素 {} 生成失败", nutrient, e);
}
}
// 4. 自动补充封面图(最多 6 张)
if (success > 0) {
try {
fillMissingCoverImages(6);
} catch (Exception e) {
log.warn("[generateNutrient] 封面图生成失败(不影响主流程)", e);
}
}
return success;
}
/**
* 构建 Coze AI 营养素科普 Prompt要求返回严格 JSON
*/
private String buildNutrientPrompt(String nutrient) {
return "请为慢性肾脏病CKD患者生成关于「" + nutrient + "」的科普内容。\n"
+ "要求严格按以下 JSON 格式返回,不要包含任何 markdown 标记或代码块:\n"
+ "{\n"
+ " \"name\": \"" + nutrient + "\",\n"
+ " \"english\": \"对应英文名\",\n"
+ " \"icon\": \"一个合适的 emoji\",\n"
+ " \"description\": \"一句话描述15字以内\",\n"
+ " \"status\": \"控制建议(如:需控制/严格控制/适量补充)\",\n"
+ " \"statusDesc\": \"状态补充说明15字以内\",\n"
+ " \"importance\": \"为什么重要2-3句话针对CKD患者\",\n"
+ " \"recommendation\": \"推荐摄入量分CKD 1-2期、3-5期未透析、透析患者三种情况说明\",\n"
+ " \"foodSources\": [\"食物1\", \"食物2\", \"食物3\", \"食物4\", \"食物5\", \"食物6\"],\n"
+ " \"riskWarning\": \"风险提示2-3句话\",\n"
+ " \"suggestions\": [\"建议1\", \"建议2\", \"建议3\", \"建议4\", \"建议5\", \"建议6\"],\n"
+ " \"disclaimer\": \"以上建议仅供参考,具体方案请咨询您的主治医生或营养师\"\n"
+ "}";
}
/**
* 从 Coze 响应中提取文本内容(取最后一条 assistant answer 消息)
*/
private String extractCozeContent(CozeBaseResponse<Object> resp) {
try {
if (resp == null || resp.getData() == null) return "AI生成失败响应为空";
Object data = resp.getData();
// CozeBaseResponse.data 通常为 Map 结构content 字段在 messages 列表中
if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) data;
Object messages = dataMap.get("messages");
if (messages instanceof List) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> msgList = (List<Map<String, Object>>) messages;
for (int i = msgList.size() - 1; i >= 0; i--) {
Map<String, Object> m = msgList.get(i);
if ("assistant".equals(m.get("role")) && "answer".equals(m.get("type"))) {
return String.valueOf(m.get("content"));
}
}
}
// fallback: 直接取 content
Object content = dataMap.get("content");
if (content != null) return String.valueOf(content);
}
return String.valueOf(data);
} catch (Exception e) {
log.error("[extractCozeContent] 解析失败", e);
return "AI生成失败" + e.getMessage();
}
}
}

View File

@@ -50,5 +50,13 @@ public interface ToolKnowledgeService {
* @return 如 total, byType (guide/article/nutrients/recipe), byStatus (published/draft/deleted)
*/
Map<String, Object> getStats();
/**
* 批量通过 Coze AI 生成 6 种营养素科普内容并写入 v2_knowledge 表(一次性管理接口)
* 已存在的营养素自动跳过,避免重复插入。
*
* @return 本次成功插入条数
*/
int generateNutrientContent();
}