|
@@ -0,0 +1,333 @@
|
|
|
|
|
+package com.etotem.cfc.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.etotem.cfc.entity.AiQProfile;
|
|
|
|
|
+import com.etotem.cfc.entity.AiQScene;
|
|
|
|
|
+import com.etotem.cfc.entity.AiQSession;
|
|
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
|
|
+import com.etotem.cfc.mapper.AiQProfileMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.AiQSceneMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.AiQSessionMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
|
|
+import com.etotem.cfc.service.AiGateway;
|
|
|
|
|
+import com.etotem.cfc.service.AiQuestionnaireService;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+@Service("aiQuestionnaireService")
|
|
|
|
|
+public class AiQuestionnaireServiceImpl implements AiQuestionnaireService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(AiQuestionnaireServiceImpl.class);
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiQSceneMapper aiQSceneMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiQSessionMapper aiQSessionMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiQProfileMapper aiQProfileMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private AiGateway aiGateway;
|
|
|
|
|
+
|
|
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
|
+
|
|
|
|
|
+ // ── 场景管理 ──
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public AiQScene saveScene(AiQScene scene, Long adminId) {
|
|
|
|
|
+ scene.setUpdatedAt(new Date());
|
|
|
|
|
+ if (scene.getId() != null) {
|
|
|
|
|
+ aiQSceneMapper.updateById(scene);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ scene.setCreatedAt(new Date());
|
|
|
|
|
+ if (scene.getEnabled() == null) scene.setEnabled(1);
|
|
|
|
|
+ if (scene.getMaxQuestions() == null) scene.setMaxQuestions(12);
|
|
|
|
|
+ aiQSceneMapper.insert(scene);
|
|
|
|
|
+ }
|
|
|
|
|
+ return scene;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<AiQScene> listScenes(Boolean enabledOnly) {
|
|
|
|
|
+ List<AiQScene> all = aiQSceneMapper.selectList(null);
|
|
|
|
|
+ if (enabledOnly == null || !enabledOnly) return all;
|
|
|
|
|
+ List<AiQScene> result = new ArrayList<>();
|
|
|
|
|
+ for (AiQScene s : all) {
|
|
|
|
|
+ if (s.getEnabled() != null && s.getEnabled() == 1) result.add(s);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void deleteScene(Long id, Long adminId) {
|
|
|
|
|
+ Long cnt = aiQSessionMapper.selectCount(new LambdaQueryWrapper<AiQSession>()
|
|
|
|
|
+ .eq(AiQSession::getSceneId, id));
|
|
|
|
|
+ if (cnt != null && cnt > 0) {
|
|
|
|
|
+ throw new RuntimeException("该场景已有问卷记录,请改用禁用");
|
|
|
|
|
+ }
|
|
|
|
|
+ aiQSceneMapper.deleteById(id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ── 会话流程 ──
|
|
|
|
|
+
|
|
|
|
|
+ private AiQScene requireScene(Long sceneId) {
|
|
|
|
|
+ AiQScene scene = aiQSceneMapper.selectById(sceneId);
|
|
|
|
|
+ if (scene == null) throw new RuntimeException("场景不存在");
|
|
|
|
|
+ if (scene.getEnabled() == null || scene.getEnabled() != 1) throw new RuntimeException("场景未启用");
|
|
|
|
|
+ return scene;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private FamilyMember requireMember(Long userId, Long memberId) {
|
|
|
|
|
+ FamilyMember member = familyMemberMapper.selectById(memberId);
|
|
|
|
|
+ if (member == null) throw new RuntimeException("家庭成员不存在");
|
|
|
|
|
+ return member;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> toSceneMap(AiQScene s) {
|
|
|
|
|
+ Map<String, Object> m = new HashMap<>();
|
|
|
|
|
+ m.put("scene_key", s.getSceneKey());
|
|
|
|
|
+ m.put("scene_name", s.getSceneName());
|
|
|
|
|
+ m.put("opening_prompt", s.getOpeningPrompt());
|
|
|
|
|
+ try {
|
|
|
|
|
+ m.put("dimensions_json", objectMapper.readValue(
|
|
|
|
|
+ s.getDimensionsJson() == null ? "{}" : s.getDimensionsJson(), Map.class));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ m.put("dimensions_json", new HashMap<>());
|
|
|
|
|
+ }
|
|
|
|
|
+ m.put("kb_scope", Arrays.asList(
|
|
|
|
|
+ s.getKbScope() == null ? "microbiome" : s.getKbScope().split(",")));
|
|
|
|
|
+ m.put("max_questions", s.getMaxQuestions() == null ? 12 : s.getMaxQuestions());
|
|
|
|
|
+ m.put("system_prompt", s.getSystemPrompt());
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Map<String, Object>> parseHistory(AiQSession session) {
|
|
|
|
|
+ List<Map<String, Object>> history = new ArrayList<>();
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (session.getHistoryJson() != null && !session.getHistoryJson().isEmpty()) {
|
|
|
|
|
+ history = objectMapper.readValue(session.getHistoryJson(),
|
|
|
|
|
+ objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("解析会话历史失败: sessionId={}", session.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ return history;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> parseQuestion(String json) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return json == null ? null : objectMapper.readValue(json, Map.class);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Map<String, Object> start(Long userId, Long sceneId, Long memberId) {
|
|
|
|
|
+ AiQScene scene = requireScene(sceneId);
|
|
|
|
|
+ FamilyMember member = requireMember(userId, memberId);
|
|
|
|
|
+
|
|
|
|
|
+ AiQSession session = new AiQSession();
|
|
|
|
|
+ session.setSceneId(sceneId);
|
|
|
|
|
+ session.setUserId(userId);
|
|
|
|
|
+ session.setMemberId(memberId);
|
|
|
|
|
+ session.setFamilyId(member.getFamilyId());
|
|
|
|
|
+ session.setStatus("running");
|
|
|
|
|
+ session.setHistoryJson("[]");
|
|
|
|
|
+ session.setQuestionCount(0);
|
|
|
|
|
+ session.setCreatedAt(new Date());
|
|
|
|
|
+ aiQSessionMapper.insert(session);
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> history = new ArrayList<>();
|
|
|
|
|
+ Map<String, Object> resp = aiGateway.advanceQuestionnaire(toSceneMap(scene), history);
|
|
|
|
|
+ Map<String, Object> question;
|
|
|
|
|
+ if (resp != null && resp.get("question") != null) {
|
|
|
|
|
+ question = (Map<String, Object>) resp.get("question");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ question = fallbackQuestion(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ session.setCurrentQuestionJson(toJson(question));
|
|
|
|
|
+ aiQSessionMapper.updateById(session);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("sessionId", session.getId());
|
|
|
|
|
+ result.put("question", question);
|
|
|
|
|
+ result.put("answeredCount", 0);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Map<String, Object> answer(Long userId, Long sessionId, String answer) {
|
|
|
|
|
+ AiQSession session = aiQSessionMapper.selectById(sessionId);
|
|
|
|
|
+ if (session == null) throw new RuntimeException("会话不存在");
|
|
|
|
|
+ if (!"running".equals(session.getStatus())) throw new RuntimeException("问卷已完成");
|
|
|
|
|
+ if (answer == null || answer.trim().isEmpty()) throw new RuntimeException("请先作答");
|
|
|
|
|
+
|
|
|
|
|
+ AiQScene scene = requireScene(session.getSceneId());
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> history = parseHistory(session);
|
|
|
|
|
+ Map<String, Object> current = parseQuestion(session.getCurrentQuestionJson());
|
|
|
|
|
+ Map<String, Object> item = new LinkedHashMap<>();
|
|
|
|
|
+ item.put("question", current == null ? fallbackQuestion(history.size()) : current);
|
|
|
|
|
+ item.put("answer", answer.trim());
|
|
|
|
|
+ history.add(item);
|
|
|
|
|
+ session.setHistoryJson(toJson(history));
|
|
|
|
|
+ session.setQuestionCount(history.size());
|
|
|
|
|
+
|
|
|
|
|
+ int max = scene.getMaxQuestions() == null ? 12 : scene.getMaxQuestions();
|
|
|
|
|
+ if (history.size() >= max) {
|
|
|
|
|
+ AiQProfile profile = doGenerateProfile(session, scene, history);
|
|
|
|
|
+ session.setStatus("finished");
|
|
|
|
|
+ session.setFinishedAt(new Date());
|
|
|
|
|
+ session.setCurrentQuestionJson(null);
|
|
|
|
|
+ aiQSessionMapper.updateById(session);
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("action", "finish");
|
|
|
|
|
+ result.put("finished", true);
|
|
|
|
|
+ result.put("profile", toProfileMap(profile));
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> resp = aiGateway.advanceQuestionnaire(toSceneMap(scene), history);
|
|
|
|
|
+ Map<String, Object> question;
|
|
|
|
|
+ if (resp != null && resp.get("question") != null) {
|
|
|
|
|
+ question = (Map<String, Object>) resp.get("question");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ question = fallbackQuestion(history.size());
|
|
|
|
|
+ }
|
|
|
|
|
+ session.setCurrentQuestionJson(toJson(question));
|
|
|
|
|
+ aiQSessionMapper.updateById(session);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
|
|
+ result.put("action", "ask");
|
|
|
|
|
+ result.put("finished", false);
|
|
|
|
|
+ result.put("question", question);
|
|
|
|
|
+ result.put("answeredCount", history.size());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public AiQProfile finish(Long userId, Long sessionId) {
|
|
|
|
|
+ AiQSession session = aiQSessionMapper.selectById(sessionId);
|
|
|
|
|
+ if (session == null) throw new RuntimeException("会话不存在");
|
|
|
|
|
+ if ("finished".equals(session.getStatus())) {
|
|
|
|
|
+ return aiQProfileMapper.selectOne(new LambdaQueryWrapper<AiQProfile>()
|
|
|
|
|
+ .eq(AiQProfile::getSessionId, sessionId));
|
|
|
|
|
+ }
|
|
|
|
|
+ AiQScene scene = requireScene(session.getSceneId());
|
|
|
|
|
+ List<Map<String, Object>> history = parseHistory(session);
|
|
|
|
|
+ if (history.isEmpty()) throw new RuntimeException("尚无回答,无法生成画像");
|
|
|
|
|
+
|
|
|
|
|
+ AiQProfile profile = doGenerateProfile(session, scene, history);
|
|
|
|
|
+ session.setStatus("finished");
|
|
|
|
|
+ session.setFinishedAt(new Date());
|
|
|
|
|
+ session.setCurrentQuestionJson(null);
|
|
|
|
|
+ aiQSessionMapper.updateById(session);
|
|
|
|
|
+ return profile;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AiQProfile doGenerateProfile(AiQSession session, AiQScene scene, List<Map<String, Object>> history) {
|
|
|
|
|
+ Map<String, Object> resp = aiGateway.generateProfile(toSceneMap(scene), history);
|
|
|
|
|
+ AiQProfile profile = new AiQProfile();
|
|
|
|
|
+ profile.setSessionId(session.getId());
|
|
|
|
|
+ profile.setSceneId(session.getSceneId());
|
|
|
|
|
+ profile.setMemberId(session.getMemberId());
|
|
|
|
|
+ if (resp != null && resp.get("profile") != null) {
|
|
|
|
|
+ Map<String, Object> p = (Map<String, Object>) resp.get("profile");
|
|
|
|
|
+ Object up = p.get("user_profile");
|
|
|
|
|
+ Object np = p.get("need_profile");
|
|
|
|
|
+ profile.setUserProfileJson(toJson(up == null ? Collections.emptyList() : up));
|
|
|
|
|
+ profile.setNeedProfileJson(toJson(np == null ? Collections.emptyList() : np));
|
|
|
|
|
+ profile.setKbUsed(Boolean.TRUE.equals(resp.get("kb_used")) ? 1 : 0);
|
|
|
|
|
+ profile.setRawResult(toJson(p));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new RuntimeException("画像生成失败,请稍后重试");
|
|
|
|
|
+ }
|
|
|
|
|
+ profile.setCreatedAt(new Date());
|
|
|
|
|
+ aiQProfileMapper.insert(profile);
|
|
|
|
|
+ return aiQProfileMapper.selectById(profile.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public Map<String, Object> getProfileDetail(Long userId, Long sessionId) {
|
|
|
|
|
+ AiQSession session = aiQSessionMapper.selectById(sessionId);
|
|
|
|
|
+ if (session == null) throw new RuntimeException("会话不存在");
|
|
|
|
|
+ AiQProfile profile = aiQProfileMapper.selectOne(new LambdaQueryWrapper<AiQProfile>()
|
|
|
|
|
+ .eq(AiQProfile::getSessionId, sessionId));
|
|
|
|
|
+ if (profile == null) throw new RuntimeException("画像不存在");
|
|
|
|
|
+ return toProfileMap(profile);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public List<AiQSession> getHistory(Long userId, Long memberId, Long sceneId) {
|
|
|
|
|
+ LambdaQueryWrapper<AiQSession> qw = new LambdaQueryWrapper<AiQSession>()
|
|
|
|
|
+ .eq(AiQSession::getMemberId, memberId)
|
|
|
|
|
+ .orderByDesc(AiQSession::getUpdatedAt);
|
|
|
|
|
+ if (sceneId != null) qw.eq(AiQSession::getSceneId, sceneId);
|
|
|
|
|
+ return aiQSessionMapper.selectList(qw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void abort(Long userId, Long sessionId) {
|
|
|
|
|
+ AiQSession session = aiQSessionMapper.selectById(sessionId);
|
|
|
|
|
+ if (session == null) return;
|
|
|
|
|
+ if ("running".equals(session.getStatus())) {
|
|
|
|
|
+ session.setStatus("aborted");
|
|
|
|
|
+ aiQSessionMapper.updateById(session);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ── 工具方法 ──
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> fallbackQuestion(int index) {
|
|
|
|
|
+ Map<String, Object> q = new LinkedHashMap<>();
|
|
|
|
|
+ q.put("id", "fb" + (index + 1));
|
|
|
|
|
+ q.put("type", "text");
|
|
|
|
|
+ q.put("text", "请简单描述您最近一周的饮食和作息情况。");
|
|
|
|
|
+ return q;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String toJson(Object o) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return objectMapper.writeValueAsString(o);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return "{}";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> toProfileMap(AiQProfile p) {
|
|
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
|
|
+ m.put("id", p.getId());
|
|
|
|
|
+ m.put("sessionId", p.getSessionId());
|
|
|
|
|
+ m.put("memberId", p.getMemberId());
|
|
|
|
|
+ try {
|
|
|
|
|
+ m.put("userProfile", p.getUserProfileJson() == null ? Collections.emptyList()
|
|
|
|
|
+ : objectMapper.readValue(p.getUserProfileJson(), List.class));
|
|
|
|
|
+ m.put("needProfile", p.getNeedProfileJson() == null ? Collections.emptyList()
|
|
|
|
|
+ : objectMapper.readValue(p.getNeedProfileJson(), List.class));
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ m.put("userProfile", Collections.emptyList());
|
|
|
|
|
+ m.put("needProfile", Collections.emptyList());
|
|
|
|
|
+ }
|
|
|
|
|
+ m.put("kbUsed", p.getKbUsed());
|
|
|
|
|
+ m.put("createdAt", p.getCreatedAt());
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|