# 用户画像驱动推荐系统设计 ## 1. 概述 利用 AI 动态问卷引擎生成的用户画像(维度得分 + 需求),建立结构化的画像存储,并通过多维度加权匹配算法向用户推荐商品、文章和活动。 ## 2. 背景与现状 ### 2.1 已有基础设施 | 组件 | 现状 | 说明 | |------|------|------| | `AiQProfile` | `userProfileJson` / `needProfileJson` JSON 存储 | 画像原始快照,不可查询 | | `ProductDimensionMapping` | 商品-维度映射表(dimensionCode, matchScore, matchReason) | 仅覆盖商品 | | `RecommendationService` | 基于标签关键词的跨表搜索 | 无维度权重计算 | | `ContentRecommendService` | 基于 dimensionCode 筛选推荐 | 权重单一,无多维度聚合 | | `RecommendationResult` | 统一推荐结果 DTO | 格式可用 | | `RecommendationLog` | 行为追踪表(show/click/consume) | 可用 | ### 2.2 核心缺口 - 画像维度得分未结构化存储,无法被推荐系统查询 - 推荐引擎只做标签匹配,没有利用维度权重做多维度聚合评分 - 文章和活动缺少统一的维度映射表 ## 3. 架构 ``` ┌──────────────────────────────────────────────────────────────────┐ │ AI 问卷完成(AiQuestionnaireService.finish) │ │ ├─ doGenerateProfile() 写 AiQProfile(原始 JSON 快照,不变) │ │ └─ finish() 末尾调用 profileDimensionService.syncFromProfile() │ │ → 同步写入 member_profile_dimension(结构化维度得分) │ └──────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ ProfileRecommendService.recommend(memberId, types, limit) │ │ │ │ 1. 查 member_profile_dimension(取最新 session 的画像) │ │ 2. 对每种 content type: │ │ a. 查对应的 dimension_mapping 表 │ │ b. 计算加权匹配得分 = Σ(profile_dim × content_dim_match) │ │ c. 需求加成:匹配 need 的维度 +20 分 │ │ 3. 合并排序 → 取 top N,hasMore = (total > limit) │ │ 4. 记录推荐日志(show) │ └──────────────────────────┬───────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ 推荐端点 /api/recommend/profile(在 RecommendationController 中) │ │ 统一返回 RecommendItemVO 列表 │ └──────────────────────────────────────────────────────────────────┘ ``` ## 4. 数据模型 ### 4.1 画像维度表(新建) ```sql 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/action/wealth', score INT NOT NULL COMMENT '得分 0-100', description VARCHAR(500) COMMENT '维度描述', evidence JSON COMMENT '证据列表 ["ev1","ev2"]', profile_type VARCHAR(10) NOT NULL DEFAULT 'user' COMMENT 'user=用户画像, need=需求画像', 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_session (session_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成员画像维度得分'; ``` ### 4.2 内容维度映射表 **商品**:复用现有 `ProductDimensionMapping`(dimensionCode, matchScore, matchReason, enabled)。 **文章**(新建): ```sql CREATE TABLE article_dimension_mapping ( id BIGINT AUTO_INCREMENT PRIMARY KEY, article_id BIGINT NOT NULL, dimension_code VARCHAR(50) NOT NULL, match_score INT DEFAULT 50 COMMENT '关联度 0-100', match_reason VARCHAR(200) COMMENT '关联原因', INDEX idx_article_dim (article_id, dimension_code), INDEX idx_dim_article (dimension_code, match_score) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文章-维度映射'; ``` **活动**(新建): ```sql CREATE TABLE activity_dimension_mapping ( id BIGINT AUTO_INCREMENT PRIMARY KEY, activity_id BIGINT NOT NULL, dimension_code VARCHAR(50) NOT NULL, match_score INT DEFAULT 50 COMMENT '关联度 0-100', match_reason VARCHAR(200) COMMENT '关联原因', INDEX idx_activity_dim (activity_id, dimension_code), INDEX idx_dim_activity (dimension_code, match_score) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动-维度映射'; ``` ### 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 映射逻辑)。 **新问卷**:每次问卷完成时自动同步。 **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. 推荐算法 ### 5.1 核心公式 ``` 对每个内容项 c(类型为 t ∈ {product, article, activity}): 基础得分 = Σ( profile_dim_score_i × mapping_match_score_i ) ───────────────────────────────────────────── Σ( profile_dim_score_i ) 需求加成 = 20 × (该内容匹配的 need_profile 维度数) 最终得分 = 基础得分 + 需求加成 ``` ### 5.2 步骤 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` 记录(无有效 session)→ 按 `ContentRecommendService` 现有逻辑按 `dimensionCode` 筛选 - 无维度映射的内容 → 不参与画像推荐(可走 `RecommendationService` 标签搜索兜底) ## 6. 服务与接口 ### 6.1 ProfileRecommendService(新建) ```java @Service public class ProfileRecommendService { public List recommend(Long memberId, List types, int limit); // 内部方法 private Long getLatestSessionId(Long memberId); private List loadUserProfiles(Long sessionId); private List loadNeedProfiles(Long sessionId); private List recommendByDimension( Long memberId, String type, List profiles, int limit); private double calcScore(List dims, int matchScore, boolean isNeedMatch); } ``` ### 6.2 AiQuestionnaireService 改造 `finish()` 方法末尾(非 `doGenerateProfile()` 内部)追加同步调用: ```java // 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("/profile") public Result> profileRecommend( @RequestBody Map params, @RequestAttribute("userId") Long userId) { // 参数: 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 管理端增加文章/活动的维度映射编辑: - 文章编辑页(`ArticleEdit.vue`)增加维度选择器(多选 + 匹配度滑块 + 原因输入) - 活动编辑页(`ActivityEdit.vue`)同理 - 复用 `ProductDimensionMapping` 的 UI 模式(已有 `ProductEdit.vue` 中的实现参考) - 后端新增 `/api/admin/article/{id}/dimensions` 和 `/api/admin/activity/{id}/dimensions` 接口 ## 7. 推荐日志 复用现有 `RecommendationLog` 表,追加 `recommend_type` 字段: | 字段 | 值 | |------|-----| | `contentType` | `article` / `product` / `activity` | | `action` | `show` / `click` / `consume` | | `recommendScore` | 算法计算的最终得分 | | `recommendType`(新增)| `profile`(画像驱动)/ `dimension`(维度筛选)/ `tag`(标签搜索) | 迁移:`ALTER TABLE recommendation_logs ADD COLUMN recommend_type VARCHAR(20) DEFAULT 'tag' COMMENT '推荐方式'` ## 8. 实施步骤 ### 阶段 1:数据层 1. 建表迁移:`member_profile_dimension` + `article_dimension_mapping` + `activity_dimension_mapping` 2. `RecommendationLog` 追加 `recommend_type` 字段 3. 现有数据迁移脚本(Article/Activity 维度映射,含 key→dimension_code 解析) ### 阶段 2:后端服务 4. 创建 `MemberProfileDimension` Entity + Mapper 5. 创建 `ArticleDimensionMapping` / `ActivityDimensionMapping` Entity + Mapper 6. 创建 `ProfileDimensionService`(画像维度同步 + key→dimension_code 映射) 7. 创建 `ProfileRecommendService`(推荐引擎核心) 8. 改造 `AiQuestionnaireService.finish()` 接入同步 ### 阶段 3:控制器与前端 9. 新增 `/api/recommend/profile` 端点(ProfileRecommendController) 10. 管理端文章/活动维度映射编辑页(ArticleEdit.vue / ActivityEdit.vue 扩展) 11. 小程序画像结果页接入推荐(`pages/mind-detail/emotion-report.vue` 或独立推荐卡片) ## 9. 约束与边界 - **维度编码**:五维体系为 `body/mind/wisdom/action/wealth`(action = 行维度,非 social) - **维度映射**:AI 问卷 `dimensions_json` 中的 key 通过关键词匹配规则映射到五维编码(见 §4.3) - **并发**:画像同步在 `finish()` 末尾串行执行,不额外加锁(画像只写一次) - **降级**:画像维度为空时走原始推荐逻辑 - **不涉及**:实时画像更新、协同过滤、向量检索 ## 10. 测试策略 - 单元测试:`ProfileRecommendService` 的加权得分计算逻辑(含需求加成) - 单元测试:`ProfileDimensionService.syncFromProfile()` 的 key→dimension_code 映射正确性 - 集成测试:画像同步 → 推荐查询 → 结果排序 → 日志记录 - 迁移测试:`mvn clean compile` + 迁移幂等性验证(INSERT IGNORE 可重复)