Переглянути джерело

refactor(backend): 删除AI问卷与画像全部13个文件

删除 AiQuestionnaireController/Service/Impl、AiQScene/AiQSession/AiQProfile实体与Mapper、MemberProfileDimension实体与Mapper、ProfileDimensionService、ProfileRecommendService

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 1 місяць тому
батько
коміт
d55e131a57

+ 0 - 159
cfc-backend/src/main/java/com/etotem/cfc/controller/AiQuestionnaireController.java

@@ -1,159 +0,0 @@
-package com.etotem.cfc.controller;
-
-import com.etotem.cfc.common.Result;
-import com.etotem.cfc.entity.AiQProfile;
-import com.etotem.cfc.entity.AiQScene;
-import com.etotem.cfc.entity.AiQSession;
-import com.etotem.cfc.service.AiQuestionnaireService;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.util.List;
-import java.util.Map;
-import com.etotem.cfc.util.ParamUtils;
-
-/**
- * AI 动态问卷引擎 — 会话/画像/场景管理
- */
-@Tag(name = "AI动态问卷引擎", description = "动态问卷引擎:场景管理、会话 start/answer/finish、画像查询")
-@RestController
-@RequestMapping("/api/ai-questionnaire")
-public class AiQuestionnaireController {
-
-    @Resource
-    private AiQuestionnaireService aiQuestionnaireService;
-
-    // ── 场景管理(admin)──
-
-    @Operation(summary = "场景列表")
-    @PostMapping("/scene/list")
-    public Result<List<AiQScene>> sceneList(
-            @RequestBody(required = false) Map<String, Object> params,
-            @RequestAttribute("userId") Long userId) {
-        boolean enabledOnly = params != null && Boolean.TRUE.equals(params.get("enabledOnly"));
-        return Result.success(aiQuestionnaireService.listScenes(enabledOnly));
-    }
-
-    @Operation(summary = "保存场景(admin)")
-    @PostMapping("/scene/save")
-    public Result<AiQScene> sceneSave(
-            @RequestBody AiQScene scene,
-            @RequestAttribute("role") String role,
-            @RequestAttribute("userId") Long userId) {
-        if (!"admin".equals(role)) return Result.error("仅管理员可操作");
-        return Result.success(aiQuestionnaireService.saveScene(scene, userId));
-    }
-
-    @Operation(summary = "删除场景(admin)")
-    @PostMapping("/scene/delete")
-    public Result<Void> sceneDelete(
-            @RequestBody Map<String, Object> params,
-            @RequestAttribute("role") String role,
-            @RequestAttribute("userId") Long userId) {
-        if (!"admin".equals(role)) return Result.error("仅管理员可操作");
-        Object id = params.get("id");
-        if (id == null) return Result.error("缺少 id");
-        try {
-            aiQuestionnaireService.deleteScene(Long.valueOf(id.toString()), userId);
-            return Result.success(null);
-        } catch (RuntimeException e) {
-            return Result.error(e.getMessage());
-        }
-    }
-
-    // ── 会话 ──
-
-    @Operation(summary = "开始问卷")
-    @PostMapping("/start")
-    public Result<Map<String, Object>> start(
-            @RequestBody Map<String, Object> params,
-            @RequestAttribute("userId") Long userId) {
-        Long sceneId = ParamUtils.getLong(params.get("sceneId"));
-        Long memberId = ParamUtils.getLong(params.get("memberId"));
-        if (sceneId == null || memberId == null) return Result.error("缺少 sceneId/memberId");
-        try {
-            return Result.success(aiQuestionnaireService.start(userId, sceneId, memberId));
-        } catch (RuntimeException e) {
-            return Result.error(e.getMessage());
-        }
-    }
-
-    @Operation(summary = "作答")
-    @PostMapping("/answer")
-    public Result<Map<String, Object>> answer(
-            @RequestBody Map<String, Object> params,
-            @RequestAttribute("userId") Long userId) {
-        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
-        Object answer = params.get("answer");
-        if (sessionId == null || answer == null) return Result.error("缺少 sessionId/answer");
-        try {
-            return Result.success(aiQuestionnaireService.answer(userId, sessionId, String.valueOf(answer)));
-        } catch (RuntimeException e) {
-            return Result.error(e.getMessage());
-        }
-    }
-
-    @Operation(summary = "结束问卷并生成画像")
-    @PostMapping("/finish")
-    public Result<Map<String, Object>> finish(
-            @RequestBody Map<String, Object> params,
-            @RequestAttribute("userId") Long userId) {
-        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
-        if (sessionId == null) return Result.error("缺少 sessionId");
-        try {
-            AiQProfile profile = aiQuestionnaireService.finish(userId, sessionId);
-            return Result.success(convertProfile(profile));
-        } catch (RuntimeException e) {
-            return Result.error(e.getMessage());
-        }
-    }
-
-    @Operation(summary = "画像详情")
-    @PostMapping("/profile/detail")
-    public Result<Map<String, Object>> profileDetail(
-            @RequestBody Map<String, Object> params,
-            @RequestAttribute("userId") Long userId) {
-        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
-        if (sessionId == null) return Result.error("缺少 sessionId");
-        try {
-            return Result.success(aiQuestionnaireService.getProfileDetail(userId, sessionId));
-        } catch (RuntimeException e) {
-            return Result.error(e.getMessage());
-        }
-    }
-
-    @Operation(summary = "会话历史")
-    @PostMapping("/history")
-    public Result<List<AiQSession>> history(
-            @RequestBody Map<String, Object> params,
-            @RequestAttribute("userId") Long userId) {
-        Long memberId = ParamUtils.getLong(params.get("memberId"));
-        Long sceneId = ParamUtils.getLong(params.get("sceneId"));
-        return Result.success(aiQuestionnaireService.getHistory(userId, memberId, sceneId));
-    }
-
-    @Operation(summary = "中止问卷")
-    @PostMapping("/abort")
-    public Result<Void> abort(
-            @RequestBody Map<String, Object> params,
-            @RequestAttribute("userId") Long userId) {
-        Long sessionId = ParamUtils.getLong(params.get("sessionId"));
-        if (sessionId == null) return Result.error("缺少 sessionId");
-        aiQuestionnaireService.abort(userId, sessionId);
-        return Result.success(null);
-    }
-
-    private Map<String, Object> convertProfile(AiQProfile profile) {
-        java.util.LinkedHashMap<String, Object> m = new java.util.LinkedHashMap<>();
-        m.put("id", profile.getId());
-        m.put("sessionId", profile.getSessionId());
-        m.put("memberId", profile.getMemberId());
-        m.put("userProfileJson", profile.getUserProfileJson());
-        m.put("needProfileJson", profile.getNeedProfileJson());
-        m.put("kbUsed", profile.getKbUsed());
-        m.put("createdAt", profile.getCreatedAt());
-        return m;
-    }
-}

+ 0 - 22
cfc-backend/src/main/java/com/etotem/cfc/entity/AiQProfile.java

@@ -1,22 +0,0 @@
-package com.etotem.cfc.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import java.util.Date;
-
-@Data
-@TableName("ai_q_profile")
-public class AiQProfile {
-    @TableId(type = IdType.AUTO)
-    private Long id;
-    private Long sessionId;
-    private Long sceneId;
-    private Long memberId;
-    private String userProfileJson;
-    private String needProfileJson;
-    private String rawResult;
-    private Integer kbUsed;
-    private Date createdAt;
-}

+ 0 - 25
cfc-backend/src/main/java/com/etotem/cfc/entity/AiQScene.java

@@ -1,25 +0,0 @@
-package com.etotem.cfc.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import java.util.Date;
-
-@Data
-@TableName("ai_q_scene")
-public class AiQScene {
-    @TableId(type = IdType.AUTO)
-    private Long id;
-    private String sceneKey;
-    private String sceneName;
-    private String description;
-    private String openingPrompt;
-    private String dimensionsJson;
-    private String kbScope;
-    private Integer maxQuestions;
-    private String systemPrompt;
-    private Integer enabled;
-    private Date createdAt;
-    private Date updatedAt;
-}

+ 0 - 25
cfc-backend/src/main/java/com/etotem/cfc/entity/AiQSession.java

@@ -1,25 +0,0 @@
-package com.etotem.cfc.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-import java.util.Date;
-
-@Data
-@TableName("ai_q_session")
-public class AiQSession {
-    @TableId(type = IdType.AUTO)
-    private Long id;
-    private Long sceneId;
-    private Long userId;
-    private Long memberId;
-    private Long familyId;
-    private String status;
-    private String historyJson;
-    private String currentQuestionJson;
-    private Integer questionCount;
-    private Date createdAt;
-    private Date finishedAt;
-    private Date updatedAt;
-}

+ 0 - 37
cfc-backend/src/main/java/com/etotem/cfc/entity/MemberProfileDimension.java

@@ -1,37 +0,0 @@
-package com.etotem.cfc.entity;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.Date;
-
-@Data
-@TableName("member_profile_dimension")
-public class MemberProfileDimension implements Serializable {
-
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    private Long memberId;
-
-    private Long sessionId;
-
-    private Long sceneId;
-
-    private String dimensionCode;
-
-    private Integer score;
-
-    private String description;
-
-    private String evidence;
-
-    private String profileType;
-
-    private Date createdAt;
-
-    private Date updatedAt;
-}

+ 0 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/AiQProfileMapper.java

@@ -1,9 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.AiQProfile;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface AiQProfileMapper extends BaseMapper<AiQProfile> {
-}

+ 0 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/AiQSceneMapper.java

@@ -1,9 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.AiQScene;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface AiQSceneMapper extends BaseMapper<AiQScene> {
-}

+ 0 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/AiQSessionMapper.java

@@ -1,9 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.AiQSession;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface AiQSessionMapper extends BaseMapper<AiQSession> {
-}

+ 0 - 9
cfc-backend/src/main/java/com/etotem/cfc/mapper/MemberProfileDimensionMapper.java

@@ -1,9 +0,0 @@
-package com.etotem.cfc.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.etotem.cfc.entity.MemberProfileDimension;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface MemberProfileDimensionMapper extends BaseMapper<MemberProfileDimension> {
-}

+ 0 - 21
cfc-backend/src/main/java/com/etotem/cfc/service/AiQuestionnaireService.java

@@ -1,21 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.etotem.cfc.entity.AiQProfile;
-import com.etotem.cfc.entity.AiQScene;
-import com.etotem.cfc.entity.AiQSession;
-
-import java.util.List;
-import java.util.Map;
-
-public interface AiQuestionnaireService {
-    AiQScene saveScene(AiQScene scene, Long adminId);
-    List<AiQScene> listScenes(Boolean enabledOnly);
-    void deleteScene(Long id, Long adminId);
-
-    Map<String, Object> start(Long userId, Long sceneId, Long memberId);
-    Map<String, Object> answer(Long userId, Long sessionId, String answer);
-    AiQProfile finish(Long userId, Long sessionId);
-    Map<String, Object> getProfileDetail(Long userId, Long sessionId);
-    List<AiQSession> getHistory(Long userId, Long memberId, Long sceneId);
-    void abort(Long userId, Long sessionId);
-}

+ 0 - 122
cfc-backend/src/main/java/com/etotem/cfc/service/ProfileDimensionService.java

@@ -1,122 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.etotem.cfc.entity.AiQProfile;
-import com.etotem.cfc.entity.AiQSession;
-import com.etotem.cfc.entity.MemberProfileDimension;
-import com.etotem.cfc.mapper.AiQSessionMapper;
-import com.etotem.cfc.mapper.MemberProfileDimensionMapper;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.Resource;
-
-import java.util.*;
-
-@Service
-public class ProfileDimensionService extends ServiceImpl<MemberProfileDimensionMapper, MemberProfileDimension> {
-
-    @Resource
-    private AiQSessionMapper aiQSessionMapper;
-
-    private static final List<DimensionRule> DIMENSION_RULES = Arrays.asList(
-            new DimensionRule("body",   new String[]{"肠道","菌群","消化","营养","免疫","睡眠","生长发育","睡眠质量","视力","免疫力","营养均衡","肠胃","运动","exercise","sleep","growth","vision","immunity","nutrition","gut"}),
-            new DimensionRule("mind",   new String[]{"压力","焦虑","情绪","抑郁","自信","神经质","情绪稳定","开放性","尽责性","外向性","宜人性","neurotic","openness","conscientiousness","extraversion","agreeableness","emotion"}),
-            new DimensionRule("wisdom", new String[]{"认知","学习","专注","记忆","思维","逻辑","空间","加工速度","感知","成长型思维","cognitive","focus","memory","logic","spatial","processing","perception","growthMindset"}),
-            new DimensionRule("action", new String[]{"亲子","人际","社交","沟通","关系","师生","夫妻","长辈","social","interpersonal","parentChild","teacherStudent","marital"}),
-            new DimensionRule("wealth", new String[]{"财富","收入","规划","储蓄","消费","积分","任务","社交积分","学习积分","wealth","income","saving","points","education","social"})
-    );
-
-    public void syncFromProfile(AiQProfile profile, AiQSession session) {
-        if (profile == null || session == null) return;
-        Long memberId = session.getMemberId();
-        Long sessionId = session.getId();
-        Long sceneId = session.getSceneId();
-
-        deleteBySession(sessionId);
-
-        List<Map<String, Object>> userDims = parseJsonArray(profile.getUserProfileJson());
-        List<Map<String, Object>> needDims = parseJsonArray(profile.getNeedProfileJson());
-
-        for (Map<String, Object> dim : userDims) {
-            insertDimension(memberId, sessionId, sceneId, dim, "user");
-        }
-        for (Map<String, Object> dim : needDims) {
-            insertDimension(memberId, sessionId, sceneId, dim, "need");
-        }
-    }
-
-    private void insertDimension(Long memberId, Long sessionId, Long sceneId,
-                                  Map<String, Object> dim, String profileType) {
-        String key = (String) dim.get("key");
-        if (key == null || key.isEmpty()) return;
-
-        String dimCode = matchDimension(key);
-        if (dimCode == null) return;
-
-        Integer score = dim.get("score") instanceof Number ? ((Number) dim.get("score")).intValue() : null;
-        if (score == null) score = 50;
-
-        String description = (String) dim.get("description");
-        if (description == null) description = (String) dim.get("key");
-
-        MemberProfileDimension entity = new MemberProfileDimension();
-        entity.setMemberId(memberId);
-        entity.setSessionId(sessionId);
-        entity.setSceneId(sceneId);
-        entity.setDimensionCode(dimCode);
-        entity.setScore(score);
-        entity.setDescription(description);
-        entity.setProfileType(profileType);
-        save(entity);
-    }
-
-    private String matchDimension(String key) {
-        for (DimensionRule rule : DIMENSION_RULES) {
-            for (String keyword : rule.keywords) {
-                if (key.toLowerCase().contains(keyword.toLowerCase())) {
-                    return rule.code;
-                }
-            }
-        }
-        return null;
-    }
-
-    public void deleteBySession(Long sessionId) {
-        remove(new LambdaQueryWrapper<MemberProfileDimension>()
-                .eq(MemberProfileDimension::getSessionId, sessionId));
-    }
-
-    public List<MemberProfileDimension> getProfilesBySession(Long sessionId) {
-        return list(new LambdaQueryWrapper<MemberProfileDimension>()
-                .eq(MemberProfileDimension::getSessionId, sessionId));
-    }
-
-    public Long getLatestSessionId(Long memberId) {
-        AiQSession s = aiQSessionMapper.selectOne(new LambdaQueryWrapper<AiQSession>()
-                .eq(AiQSession::getMemberId, memberId)
-                .eq(AiQSession::getStatus, "finished")
-                .orderByDesc(AiQSession::getFinishedAt)
-                .last("LIMIT 1"));
-        return s != null ? s.getId() : null;
-    }
-
-    private List<Map<String, Object>> parseJsonArray(String json) {
-        if (json == null || json.isEmpty()) return Collections.emptyList();
-        try {
-            return new com.fasterxml.jackson.databind.ObjectMapper()
-                    .readValue(json, List.class);
-        } catch (Exception e) {
-            return Collections.emptyList();
-        }
-    }
-
-    private static class DimensionRule {
-        final String code;
-        final String[] keywords;
-        DimensionRule(String code, String[] keywords) {
-            this.code = code;
-            this.keywords = keywords;
-        }
-    }
-}

+ 0 - 227
cfc-backend/src/main/java/com/etotem/cfc/service/ProfileRecommendService.java

@@ -1,227 +0,0 @@
-package com.etotem.cfc.service;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.etotem.cfc.entity.*;
-import com.etotem.cfc.mapper.*;
-import org.springframework.stereotype.Service;
-
-import javax.annotation.Resource;
-import java.util.*;
-
-@Service
-public class ProfileRecommendService {
-
-    @Resource private MemberProfileDimensionMapper profileDimMapper;
-    @Resource private AiQSessionMapper sessionMapper;
-    @Resource private DimensionWeightService dimensionWeightService;
-    @Resource private ArticleMapper articleMapper;
-    @Resource private ActivityMapper activityMapper;
-    @Resource private ProductMapper productMapper;
-    @Resource private RecommendationLogMapper recommendationLogMapper;
-    @Resource private ProductDimensionMappingMapper productDimMapper;
-
-    public Map<String, Object> recommend(Long memberId, List<String> types, int limit) {
-        Long sessionId = getLatestFinishedSession(memberId);
-        if (sessionId == null) {
-            return emptyResult(types, limit);
-        }
-
-        List<MemberProfileDimension> userDims = profileDimMapper.selectList(
-                new LambdaQueryWrapper<MemberProfileDimension>()
-                        .eq(MemberProfileDimension::getSessionId, sessionId)
-                        .eq(MemberProfileDimension::getProfileType, "user"));
-        List<MemberProfileDimension> needDims = profileDimMapper.selectList(
-                new LambdaQueryWrapper<MemberProfileDimension>()
-                        .eq(MemberProfileDimension::getSessionId, sessionId)
-                        .eq(MemberProfileDimension::getProfileType, "need"));
-
-        Set<String> needCodes = new HashSet<>();
-        for (MemberProfileDimension d : needDims) needCodes.add(d.getDimensionCode());
-
-        List<Map<String, Object>> results = new ArrayList<>();
-        int totalAcrossTypes = 0;
-
-        for (String type : types) {
-            List<ItemScore> items = recommendByType(memberId, type, userDims, needCodes, limit * 3);
-            totalAcrossTypes += items.size();
-            for (ItemScore item : items) {
-                Map<String, Object> m = new LinkedHashMap<>();
-                m.put("id", item.id);
-                m.put("type", item.type);
-                m.put("title", item.title);
-                m.put("summary", item.summary);
-                m.put("coverImage", item.coverImage);
-                m.put("dimensionCode", item.dimensionCode);
-                m.put("recommendScore", item.score);
-                m.put("recommendReason", item.reason);
-                m.put("badge", item.badge);
-                results.add(m);
-            }
-        }
-
-        results.sort((a, b) -> Double.compare((double) b.get("recommendScore"), (double) a.get("recommendScore")));
-        if (results.size() > limit) results = results.subList(0, limit);
-
-        for (Map<String, Object> r : results) {
-            logShow(memberId, (String) r.get("type"), ((Number) r.get("id")).longValue(),
-                    ((Number) r.get("recommendScore")).doubleValue(), sessionId);
-        }
-
-        Map<String, Object> out = new LinkedHashMap<>();
-        out.put("recommendations", results);
-        out.put("hasMore", totalAcrossTypes > limit);
-        out.put("profileSessionId", sessionId);
-        return out;
-    }
-
-    private List<ItemScore> recommendByType(Long memberId, String type,
-                                             List<MemberProfileDimension> userDims,
-                                             Set<String> needCodes, int maxItems) {
-        if (userDims.isEmpty()) return Collections.emptyList();
-
-        double totalProfileScore = 0;
-        for (MemberProfileDimension d : userDims) totalProfileScore += d.getScore();
-        if (totalProfileScore == 0) return Collections.emptyList();
-
-        List<ItemScore> items = new ArrayList<>();
-
-        if ("product".equals(type)) {
-            List<ProductDimensionMapping> mappings = productDimMapper.selectList(
-                    new LambdaQueryWrapper<ProductDimensionMapping>().eq(ProductDimensionMapping::getEnabled, 1));
-            Map<Long, Double> scores = new HashMap<>();
-            for (ProductDimensionMapping m : mappings) {
-                for (MemberProfileDimension d : userDims) {
-                    if (d.getDimensionCode().equals(m.getDimensionCode())) {
-                        double base = (d.getScore() * m.getMatchScore()) / totalProfileScore;
-                        double finalScore = base + (needCodes.contains(m.getDimensionCode()) ? 20 : 0);
-                        scores.merge(m.getProductId(), finalScore, Double::sum);
-                        break;
-                    }
-                }
-            }
-            scores.forEach((pid, score) -> {
-                Product p = productMapper.selectById(pid);
-                if (p != null) items.add(new ItemScore(pid, "product", p.getName(),
-                        p.getDescription(), p.getCoverImage(), score, "高匹配"));
-            });
-
-        } else if ("article".equals(type)) {
-            Map<Long, Double> scores = new HashMap<>();
-            List<Article> articles = articleMapper.selectList(null);
-            for (Article a : articles) {
-                List<DimensionWeight> dw = dimensionWeightService.getByTarget("article", a.getId());
-                double dimScore = 0;
-                for (DimensionWeight w : dw) {
-                    if (!Boolean.TRUE.equals(w.getEnabled())) continue;
-                    for (MemberProfileDimension d : userDims) {
-                        if (d.getDimensionCode().equals(w.getDimension())) {
-                            dimScore += d.getScore() * w.getWeight();
-                            break;
-                        }
-                    }
-                }
-                if (dimScore > 0) {
-                    double base = dimScore / totalProfileScore;
-                    long matchedNeed = 0;
-                    for (DimensionWeight w : dw) {
-                        if (Boolean.TRUE.equals(w.getEnabled()) && needCodes.contains(w.getDimension())) matchedNeed++;
-                    }
-                    double finalScore = base + (20.0 * matchedNeed / Math.max(dw.size(), 1));
-                    scores.merge(a.getId(), finalScore, Double::sum);
-                }
-            }
-            scores.forEach((aid, score) -> {
-                Article a = articleMapper.selectById(aid);
-                if (a != null) items.add(new ItemScore(aid, "article", a.getTitle(),
-                        a.getSummary(), a.getCoverImage(), score, "高匹配"));
-            });
-
-        } else if ("activity".equals(type)) {
-            Map<Long, Double> scores = new HashMap<>();
-            List<Activity> activities = activityMapper.selectList(null);
-            for (Activity act : activities) {
-                List<DimensionWeight> dw = dimensionWeightService.getByTarget("activity", act.getId());
-                double dimScore = 0;
-                for (DimensionWeight w : dw) {
-                    if (!Boolean.TRUE.equals(w.getEnabled())) continue;
-                    for (MemberProfileDimension d : userDims) {
-                        if (d.getDimensionCode().equals(w.getDimension())) {
-                            dimScore += d.getScore() * w.getWeight();
-                            break;
-                        }
-                    }
-                }
-                if (dimScore > 0) {
-                    double base = dimScore / totalProfileScore;
-                    long matchedNeed = 0;
-                    for (DimensionWeight w : dw) {
-                        if (Boolean.TRUE.equals(w.getEnabled()) && needCodes.contains(w.getDimension())) matchedNeed++;
-                    }
-                    double finalScore = base + (20.0 * matchedNeed / Math.max(dw.size(), 1));
-                    scores.merge(act.getId(), finalScore, Double::sum);
-                }
-            }
-            scores.forEach((actId, score) -> {
-                Activity act = activityMapper.selectById(actId);
-                if (act != null) items.add(new ItemScore(actId, "activity", act.getTitle(),
-                        act.getDescription(), act.getCoverImage(), score, "高匹配"));
-            });
-        }
-
-        items.sort((a, b) -> Double.compare(b.score, a.score));
-        return items.size() > maxItems ? items.subList(0, maxItems) : items;
-    }
-
-    private Long getLatestFinishedSession(Long memberId) {
-        AiQSession s = sessionMapper.selectOne(new LambdaQueryWrapper<AiQSession>()
-                .eq(AiQSession::getMemberId, memberId)
-                .eq(AiQSession::getStatus, "finished")
-                .orderByDesc(AiQSession::getFinishedAt)
-                .last("LIMIT 1"));
-        return s != null ? s.getId() : null;
-    }
-
-    private Map<String, Object> emptyResult(List<String> types, int limit) {
-        Map<String, Object> out = new LinkedHashMap<>();
-        out.put("recommendations", Collections.emptyList());
-        out.put("hasMore", false);
-        out.put("profileSessionId", null);
-        return out;
-    }
-
-    private void logShow(Long memberId, String type, Long contentId, double score, Long sessionId) {
-        try {
-            RecommendationLog log = new RecommendationLog();
-            log.setUserId(memberId);
-            log.setContentType(type);
-            log.setContentId(contentId);
-            log.setRecommendScore((int) score);
-            log.setAction("show");
-            log.setRecommendType("profile");
-            recommendationLogMapper.insert(log);
-        } catch (Exception e) {
-            // log silently
-        }
-    }
-
-    private static class ItemScore {
-        final long id;
-        final String type;
-        final String title;
-        final String summary;
-        final String coverImage;
-        final double score;
-        final String reason;
-        final String badge;
-        final String dimensionCode;
-
-        ItemScore(long id, String type, String title, String summary, String coverImage,
-                  double score, String reason) {
-            this.id = id; this.type = type; this.title = title;
-            this.summary = summary; this.coverImage = coverImage;
-            this.score = score; this.reason = reason;
-            this.badge = score >= 70 ? "优秀匹配" : score >= 40 ? "一般匹配" : "低匹配";
-            this.dimensionCode = null;
-        }
-    }
-}

+ 0 - 343
cfc-backend/src/main/java/com/etotem/cfc/service/impl/AiQuestionnaireServiceImpl.java

@@ -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;
-    }
-}