利用 AI 动态问卷引擎生成的用户画像(维度得分 + 需求),建立结构化的画像存储,并通过多维度加权匹配算法向用户推荐商品、文章和活动。
| 组件 | 现状 | 说明 |
|---|---|---|
AiQProfile |
userProfileJson / needProfileJson JSON 存储 |
画像原始快照,不可查询 |
ProductDimensionMapping |
商品-维度映射表(dimensionCode, matchScore, matchReason) | 仅覆盖商品 |
RecommendationService |
基于标签关键词的跨表搜索 | 无维度权重计算 |
ContentRecommendService |
基于 dimensionCode 筛选推荐 | 权重单一,无多维度聚合 |
RecommendationResult |
统一推荐结果 DTO | 格式可用 |
RecommendationLog |
行为追踪表(show/click/consume) | 可用 |
┌──────────────────────────────────────────────────────────────────┐
│ 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 列表 │
└──────────────────────────────────────────────────────────────────┘
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='成员画像维度得分';
商品:复用现有 ProductDimensionMapping(dimensionCode, matchScore, matchReason, enabled)。
文章(新建):
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='文章-维度映射';
活动(新建):
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='活动-维度映射';
AI 问卷场景的 dimensions_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 |
映射优先级:精确匹配 > 关键词包含匹配 > 不映射(跳过该维度)。
历史画像:现有 AiQProfile 数据不入 member_profile_dimension(历史数据无 key→dimension_code 映射逻辑)。
新问卷:每次问卷完成时自动同步。
Article.dimensionIds 迁移(一次性脚本):
Article.dimensionIds(VARCHAR,逗号分隔,如 "body,mind")、dimensionWeights(VARCHAR,逗号分隔,如 "0.8,0.6")article_dimension_mapping(match_score = weight × 100 取整)Activity.dimensionCode 迁移(一次性脚本):
Activity.dimensionCode(VARCHAR,单个值如 "mind")、dimensionWeights(VARCHAR,如 "0.7")幂等性:所有迁移使用 INSERT IGNORE,可重复执行。
对每个内容项 c(类型为 t ∈ {product, article, activity}):
基础得分 = Σ( profile_dim_score_i × mapping_match_score_i )
─────────────────────────────────────────────
Σ( profile_dim_score_i )
需求加成 = 20 × (该内容匹配的 need_profile 维度数)
最终得分 = 基础得分 + 需求加成
memberId 查询最新的 ai_q_session(按 finished_at 降序取 1 条)session_id 查询 member_profile_dimension 中 profile_type='user' 的所有维度得分profile_type='need' 的维度编码集合多次画像处理:只取最新一次问卷完成后的画像(session_id 最大),避免历史低质量画像干扰推荐。
member_profile_dimension 记录(无有效 session)→ 按 ContentRecommendService 现有逻辑按 dimensionCode 筛选RecommendationService 标签搜索兜底)@Service
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(List<DimensionScore> dims, int matchScore, boolean isNeedMatch);
}
finish() 方法末尾(非 doGenerateProfile() 内部)追加同步调用:
// 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 逻辑:
profile.getUserProfileJson() 和 profile.getNeedProfileJson()(JSON 字符串)位置:RecommendationController(或新建 ProfileRecommendController),路径 /api/recommend/profile
@PostMapping("/profile")
public Result<Map<String, Object>> profileRecommend(
@RequestBody Map<String, Object> 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
// }
}
Web 管理端增加文章/活动的维度映射编辑:
ArticleEdit.vue)增加维度选择器(多选 + 匹配度滑块 + 原因输入)ActivityEdit.vue)同理ProductDimensionMapping 的 UI 模式(已有 ProductEdit.vue 中的实现参考)/api/admin/article/{id}/dimensions 和 /api/admin/activity/{id}/dimensions 接口复用现有 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 '推荐方式'
member_profile_dimension + article_dimension_mapping + activity_dimension_mappingRecommendationLog 追加 recommend_type 字段MemberProfileDimension Entity + MapperArticleDimensionMapping / ActivityDimensionMapping Entity + MapperProfileDimensionService(画像维度同步 + key→dimension_code 映射)ProfileRecommendService(推荐引擎核心)AiQuestionnaireService.finish() 接入同步/api/recommend/profile 端点(ProfileRecommendController)pages/mind-detail/emotion-report.vue 或独立推荐卡片)body/mind/wisdom/action/wealth(action = 行维度,非 social)dimensions_json 中的 key 通过关键词匹配规则映射到五维编码(见 §4.3)finish() 末尾串行执行,不额外加锁(画像只写一次)ProfileRecommendService 的加权得分计算逻辑(含需求加成)ProfileDimensionService.syncFromProfile() 的 key→dimension_code 映射正确性mvn clean compile + 迁移幂等性验证(INSERT IGNORE 可重复)