|
|
@@ -28,27 +28,27 @@
|
|
|
```
|
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
|
│ AI 问卷完成(AiQuestionnaireService.finish) │
|
|
|
-│ → doGenerateProfile() │
|
|
|
-│ ├─ 写 AiQProfile(原始 JSON 快照,不变) │
|
|
|
-│ └─ 同步写入 member_profile_dimension(结构化维度得分) │
|
|
|
+│ ├─ doGenerateProfile() 写 AiQProfile(原始 JSON 快照,不变) │
|
|
|
+│ └─ finish() 末尾调用 profileDimensionService.syncFromProfile() │
|
|
|
+│ → 同步写入 member_profile_dimension(结构化维度得分) │
|
|
|
└──────────────────────────┬───────────────────────────────────────┘
|
|
|
│
|
|
|
▼
|
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
|
│ ProfileRecommendService.recommend(memberId, types, limit) │
|
|
|
│ │
|
|
|
-│ 1. 查 member_profile_dimension(user 维度得分 + need 需求) │
|
|
|
+│ 1. 查 member_profile_dimension(取最新 session 的画像) │
|
|
|
│ 2. 对每种 content type: │
|
|
|
│ a. 查对应的 dimension_mapping 表 │
|
|
|
│ b. 计算加权匹配得分 = Σ(profile_dim × content_dim_match) │
|
|
|
│ c. 需求加成:匹配 need 的维度 +20 分 │
|
|
|
-│ 3. 合并排序 → 取 top N │
|
|
|
+│ 3. 合并排序 → 取 top N,hasMore = (total > limit) │
|
|
|
│ 4. 记录推荐日志(show) │
|
|
|
└──────────────────────────┬───────────────────────────────────────┘
|
|
|
│
|
|
|
▼
|
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
|
-│ 推荐端点 /api/recommend/profile(供小程序/Web 调用) │
|
|
|
+│ 推荐端点 /api/recommend/profile(在 RecommendationController 中) │
|
|
|
│ 统一返回 RecommendItemVO 列表 │
|
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
|
```
|
|
|
@@ -61,8 +61,9 @@
|
|
|
CREATE TABLE member_profile_dimension (
|
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
member_id BIGINT NOT NULL COMMENT '家庭成员ID',
|
|
|
+ session_id BIGINT NOT NULL COMMENT '来源会话ID(ai_q_session.id,用于取最新)',
|
|
|
scene_id BIGINT COMMENT '来源场景ID(ai_q_scene.id)',
|
|
|
- dimension_code VARCHAR(50) NOT NULL COMMENT '五维编码: body/mind/wisdom/wealth/social',
|
|
|
+ dimension_code VARCHAR(50) NOT NULL COMMENT '五维编码: body/mind/wisdom/action/wealth',
|
|
|
score INT NOT NULL COMMENT '得分 0-100',
|
|
|
description VARCHAR(500) COMMENT '维度描述',
|
|
|
evidence JSON COMMENT '证据列表 ["ev1","ev2"]',
|
|
|
@@ -70,12 +71,11 @@ CREATE TABLE member_profile_dimension (
|
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
INDEX idx_member_dim (member_id, dimension_code),
|
|
|
- INDEX idx_member_type (member_id, profile_type)
|
|
|
+ INDEX idx_member_type (member_id, profile_type),
|
|
|
+ INDEX idx_session (session_id)
|
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成员画像维度得分';
|
|
|
```
|
|
|
|
|
|
-**维度编码规则**:与五维体系一致 `body/mind/wisdom/wealth/social`,AI 问卷的 `dimensions_json` 中的维度键值映射到五维编码(如"肠道菌群状态"→"body")。
|
|
|
-
|
|
|
### 4.2 内容维度映射表
|
|
|
|
|
|
**商品**:复用现有 `ProductDimensionMapping`(dimensionCode, matchScore, matchReason, enabled)。
|
|
|
@@ -108,12 +108,50 @@ CREATE TABLE activity_dimension_mapping (
|
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动-维度映射';
|
|
|
```
|
|
|
|
|
|
-### 4.3 数据迁移
|
|
|
+### 4.3 维度编码映射规则
|
|
|
+
|
|
|
+AI 问卷场景的 `dimensions_json` 字段(JSON)结构如下:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "user": [
|
|
|
+ {"key": "肠道菌群状态", "score": 85, "description": "肠道健康良好"},
|
|
|
+ {"key": "压力倾向", "score": 72, "description": "中等压力水平"}
|
|
|
+ ],
|
|
|
+ "need": [
|
|
|
+ {"key": "焦虑倾向", "score": 68},
|
|
|
+ {"key": "消化改善", "score": 80}
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**映射规则**(key → dimension_code):
|
|
|
+
|
|
|
+| key 关键词包含 | dimension_code |
|
|
|
+|---|---|
|
|
|
+| 肠道/菌群/消化/营养/免疫/睡眠 | `body` |
|
|
|
+| 压力/焦虑/情绪/抑郁/自信/神经质 | `mind` |
|
|
|
+| 认知/学习/专注/记忆/思维 | `wisdom` |
|
|
|
+| 亲子/人际/社交/沟通/关系 | `action` |
|
|
|
+| 财富/收入/规划/储蓄/消费 | `wealth` |
|
|
|
+
|
|
|
+映射优先级:精确匹配 > 关键词包含匹配 > 不映射(跳过该维度)。
|
|
|
+
|
|
|
+### 4.4 数据迁移
|
|
|
+
|
|
|
+**历史画像**:现有 `AiQProfile` 数据不入 `member_profile_dimension`(历史数据无 key→dimension_code 映射逻辑)。
|
|
|
|
|
|
-- 现有 `AiQProfile` 数据不入 `member_profile_dimension`(历史数据无 dimension_code 映射)
|
|
|
-- 新问卷完成后自动同步
|
|
|
-- 现有 `Article.dimensionIds`/`dimensionWeights` 数据通过一次性迁移脚本导入 `article_dimension_mapping`
|
|
|
-- 现有 `Activity.dimensionCode`/`dimensionWeights` 同理
|
|
|
+**新问卷**:每次问卷完成时自动同步。
|
|
|
+
|
|
|
+**Article.dimensionIds 迁移**(一次性脚本):
|
|
|
+- 现有字段:`Article.dimensionIds`(VARCHAR,逗号分隔,如 `"body,mind"`)、`dimensionWeights`(VARCHAR,逗号分隔,如 `"0.8,0.6"`)
|
|
|
+- 迁移逻辑:按逗号拆分 → 配对 → 逐行 INSERT IGNORE INTO `article_dimension_mapping`(match_score = weight × 100 取整)
|
|
|
+
|
|
|
+**Activity.dimensionCode 迁移**(一次性脚本):
|
|
|
+- 现有字段:`Activity.dimensionCode`(VARCHAR,单个值如 `"mind"`)、`dimensionWeights`(VARCHAR,如 `"0.7"`)
|
|
|
+- 迁移逻辑:直接映射为一条记录,match_score = weight × 100
|
|
|
+
|
|
|
+**幂等性**:所有迁移使用 INSERT IGNORE,可重复执行。
|
|
|
|
|
|
## 5. 推荐算法
|
|
|
|
|
|
@@ -133,16 +171,20 @@ CREATE TABLE activity_dimension_mapping (
|
|
|
|
|
|
### 5.2 步骤
|
|
|
|
|
|
-1. 根据 `memberId` 查询 `member_profile_dimension` 中 `profile_type='user'` 的所有维度得分
|
|
|
-2. 查询 `profile_type='need'` 的维度编码集合
|
|
|
-3. 对每种请求的 content type,查询对应的 dimension_mapping 表,JOIN 步骤 1 的维度得分
|
|
|
-4. 按公式计算每项内容的最终得分
|
|
|
-5. 按最终得分降序排列,取 top limit
|
|
|
-6. 去重(同一内容可能映射多个维度,只保留最高分条目)
|
|
|
+1. 根据 `memberId` 查询最新的 `ai_q_session`(按 finished_at 降序取 1 条)
|
|
|
+2. 根据 `session_id` 查询 `member_profile_dimension` 中 `profile_type='user'` 的所有维度得分
|
|
|
+3. 查询 `profile_type='need'` 的维度编码集合
|
|
|
+4. 对每种请求的 content type,查询对应的 dimension_mapping 表,JOIN 步骤 2 的维度得分
|
|
|
+5. 按公式计算每项内容的最终得分
|
|
|
+6. 按最终得分降序排列,取 top limit
|
|
|
+7. 去重(同一内容可能映射多个维度,只保留最高分条目)
|
|
|
+8. 计算 hasMore:SELECT COUNT(*) WHERE dimension_code IN (...),若 total > limit 则 hasMore=true
|
|
|
+
|
|
|
+**多次画像处理**:只取最新一次问卷完成后的画像(session_id 最大),避免历史低质量画像干扰推荐。
|
|
|
|
|
|
### 5.3 无画像时的降级
|
|
|
|
|
|
-- 无 `member_profile_dimension` 记录 → 按 `ContentRecommendService` 现有逻辑按 `dimensionCode` 筛选
|
|
|
+- 无 `member_profile_dimension` 记录(无有效 session)→ 按 `ContentRecommendService` 现有逻辑按 `dimensionCode` 筛选
|
|
|
- 无维度映射的内容 → 不参与画像推荐(可走 `RecommendationService` 标签搜索兜底)
|
|
|
|
|
|
## 6. 服务与接口
|
|
|
@@ -154,84 +196,110 @@ CREATE TABLE activity_dimension_mapping (
|
|
|
public class ProfileRecommendService {
|
|
|
public List<RecommendItemVO> recommend(Long memberId, List<String> types, int limit);
|
|
|
// 内部方法
|
|
|
+ private Long getLatestSessionId(Long memberId);
|
|
|
+ private List<MemberProfileDimension> loadUserProfiles(Long sessionId);
|
|
|
+ private List<MemberProfileDimension> loadNeedProfiles(Long sessionId);
|
|
|
private List<RecommendItemVO> recommendByDimension(
|
|
|
Long memberId, String type, List<MemberProfileDimension> profiles, int limit);
|
|
|
- private double calcScore(int profileScore, int matchScore, boolean isNeedMatch);
|
|
|
+ private double calcScore(List<DimensionScore> dims, int matchScore, boolean isNeedMatch);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 6.2 AiQuestionnaireService 改造
|
|
|
|
|
|
-`doGenerateProfile()` 末尾追加同步调用:
|
|
|
+`finish()` 方法末尾(非 `doGenerateProfile()` 内部)追加同步调用:
|
|
|
|
|
|
```java
|
|
|
-// 同步画像维度到 member_profile_dimension
|
|
|
-profileDimensionService.syncFromProfile(profile, session);
|
|
|
+// AiQuestionnaireService.finish() 末尾
|
|
|
+public AiQProfile finish(Long userId, Long sessionId) {
|
|
|
+ AiQProfile profile = doGenerateProfile(session, scene, history);
|
|
|
+ // ... 原有保存逻辑 ...
|
|
|
+
|
|
|
+ // 新增:同步画像维度到 member_profile_dimension
|
|
|
+ profileDimensionService.syncFromProfile(profile, session);
|
|
|
+
|
|
|
+ return profile;
|
|
|
+}
|
|
|
```
|
|
|
|
|
|
+**syncFromProfile 逻辑**:
|
|
|
+1. 解析 `profile.getUserProfileJson()` 和 `profile.getNeedProfileJson()`(JSON 字符串)
|
|
|
+2. 按 §4.3 映射规则将 key → dimension_code
|
|
|
+3. DELETE 该 session_id 对应的旧记录(幂等)
|
|
|
+4. INSERT 新记录
|
|
|
+
|
|
|
### 6.3 推荐端点(新增)
|
|
|
|
|
|
+**位置**:`RecommendationController`(或新建 `ProfileRecommendController`),路径 `/api/recommend/profile`
|
|
|
+
|
|
|
```java
|
|
|
-@PostMapping("/api/recommend/profile")
|
|
|
+@PostMapping("/profile")
|
|
|
public Result<Map<String, Object>> profileRecommend(
|
|
|
@RequestBody Map<String, Object> params,
|
|
|
@RequestAttribute("userId") Long userId) {
|
|
|
- // 参数: memberId, types(可选,默认["product","article","activity"]), limit(默认10)
|
|
|
- // 返回: { recommendations: [{id, title, summary, coverImage, dimensionCode, recommendScore, recommendReason, badge}], hasMore: bool }
|
|
|
+ // 参数: memberId(可选,默认userId), types(可选,默认["product","article","activity"]), limit(默认10)
|
|
|
+ // 返回: {
|
|
|
+ // recommendations: [{id, title, summary, coverImage, dimensionCode, recommendScore, recommendReason, badge}],
|
|
|
+ // hasMore: bool,
|
|
|
+ // profileSessionId: Long // 本次推荐使用的画像会话ID
|
|
|
+ // }
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 6.4 管理端维度映射配置(新增)
|
|
|
|
|
|
Web 管理端增加文章/活动的维度映射编辑:
|
|
|
-- 文章编辑页增加维度选择器(多选 + 匹配度 + 原因)
|
|
|
-- 活动编辑页同理
|
|
|
-- 复用 `ProductDimensionMapping` 的 UI 模式
|
|
|
+- 文章编辑页(`ArticleEdit.vue`)增加维度选择器(多选 + 匹配度滑块 + 原因输入)
|
|
|
+- 活动编辑页(`ActivityEdit.vue`)同理
|
|
|
+- 复用 `ProductDimensionMapping` 的 UI 模式(已有 `ProductEdit.vue` 中的实现参考)
|
|
|
+- 后端新增 `/api/admin/article/{id}/dimensions` 和 `/api/admin/activity/{id}/dimensions` 接口
|
|
|
|
|
|
## 7. 推荐日志
|
|
|
|
|
|
-复用现有 `RecommendationLog` 表:
|
|
|
+复用现有 `RecommendationLog` 表,追加 `recommend_type` 字段:
|
|
|
|
|
|
| 字段 | 值 |
|
|
|
|------|-----|
|
|
|
| `contentType` | `article` / `product` / `activity` |
|
|
|
| `action` | `show` / `click` / `consume` |
|
|
|
| `recommendScore` | 算法计算的最终得分 |
|
|
|
-| 新增 `recommendType` | `profile`(画像驱动)/ `dimension`(维度筛选)/ `tag`(标签搜索) |
|
|
|
+| `recommendType`(新增)| `profile`(画像驱动)/ `dimension`(维度筛选)/ `tag`(标签搜索) |
|
|
|
|
|
|
-`RecommendationLog` 追加 `recommend_type` 字段。
|
|
|
+迁移:`ALTER TABLE recommendation_logs ADD COLUMN recommend_type VARCHAR(20) DEFAULT 'tag' COMMENT '推荐方式'`
|
|
|
|
|
|
## 8. 实施步骤
|
|
|
|
|
|
-### 阶段 1:数据层(2 个任务)
|
|
|
+### 阶段 1:数据层
|
|
|
|
|
|
1. 建表迁移:`member_profile_dimension` + `article_dimension_mapping` + `activity_dimension_mapping`
|
|
|
2. `RecommendationLog` 追加 `recommend_type` 字段
|
|
|
-3. 现有数据迁移脚本(article/activity 维度映射)
|
|
|
+3. 现有数据迁移脚本(Article/Activity 维度映射,含 key→dimension_code 解析)
|
|
|
|
|
|
-### 阶段 2:后端服务(2 个任务)
|
|
|
+### 阶段 2:后端服务
|
|
|
|
|
|
4. 创建 `MemberProfileDimension` Entity + Mapper
|
|
|
5. 创建 `ArticleDimensionMapping` / `ActivityDimensionMapping` Entity + Mapper
|
|
|
-6. 创建 `ProfileDimensionService`(画像维度同步)
|
|
|
+6. 创建 `ProfileDimensionService`(画像维度同步 + key→dimension_code 映射)
|
|
|
7. 创建 `ProfileRecommendService`(推荐引擎核心)
|
|
|
-8. 改造 `AiQuestionnaireService.doGenerateProfile()` 接入同步
|
|
|
+8. 改造 `AiQuestionnaireService.finish()` 接入同步
|
|
|
|
|
|
-### 阶段 3:控制器与前端(2 个任务)
|
|
|
+### 阶段 3:控制器与前端
|
|
|
|
|
|
-9. 新增 `/api/recommend/profile` 端点
|
|
|
-10. 管理端文章/活动维度映射编辑页
|
|
|
-11. 小程序画像结果页接入推荐
|
|
|
+9. 新增 `/api/recommend/profile` 端点(ProfileRecommendController)
|
|
|
+10. 管理端文章/活动维度映射编辑页(ArticleEdit.vue / ActivityEdit.vue 扩展)
|
|
|
+11. 小程序画像结果页接入推荐(`pages/mind-detail/emotion-report.vue` 或独立推荐卡片)
|
|
|
|
|
|
## 9. 约束与边界
|
|
|
|
|
|
-- **维度编码**:与五维体系一致(`body/mind/wisdom/wealth/social`),AI 问卷场景的 `dimensions_json` 中的键需映射到五维编码
|
|
|
-- **并发**:画像同步在 `doGenerateProfile` 末尾串行执行,不额外加锁(画像只写一次)
|
|
|
+- **维度编码**:五维体系为 `body/mind/wisdom/action/wealth`(action = 行维度,非 social)
|
|
|
+- **维度映射**:AI 问卷 `dimensions_json` 中的 key 通过关键词匹配规则映射到五维编码(见 §4.3)
|
|
|
+- **并发**:画像同步在 `finish()` 末尾串行执行,不额外加锁(画像只写一次)
|
|
|
- **降级**:画像维度为空时走原始推荐逻辑
|
|
|
- **不涉及**:实时画像更新、协同过滤、向量检索
|
|
|
|
|
|
## 10. 测试策略
|
|
|
|
|
|
-- 单元测试:`ProfileRecommendService` 的加权得分计算逻辑
|
|
|
-- 集成测试:画像同步 → 推荐查询 → 结果排序
|
|
|
-- 迁移测试:`mvn clean compile` + 迁移幂等性验证
|
|
|
+- 单元测试:`ProfileRecommendService` 的加权得分计算逻辑(含需求加成)
|
|
|
+- 单元测试:`ProfileDimensionService.syncFromProfile()` 的 key→dimension_code 映射正确性
|
|
|
+- 集成测试:画像同步 → 推荐查询 → 结果排序 → 日志记录
|
|
|
+- 迁移测试:`mvn clean compile` + 迁移幂等性验证(INSERT IGNORE 可重复)
|