|
|
@@ -1,343 +0,0 @@
|
|
|
-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.etotem.cfc.service.ProfileDimensionService;
|
|
|
-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 ProfileDimensionService profileDimensionService;
|
|
|
-
|
|
|
- @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);
|
|
|
-
|
|
|
- // 同步画像维度到 member_profile_dimension
|
|
|
- try { profileDimensionService.syncFromProfile(profile, session); } catch (Exception e) {
|
|
|
- log.warn("画像维度同步失败, sessionId={}: {}", session.getId(), e.getMessage());
|
|
|
- }
|
|
|
-
|
|
|
- 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;
|
|
|
- }
|
|
|
-}
|