新功能:个性化推荐算法
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { buildUserProfileFromQuestionnaire } from '../index';
|
||||
import { mapOnboardingSelectionsToQuestionnaireAnswers } from '../onboardingMapping';
|
||||
|
||||
describe('Onboarding → UserProfileScoring 集成', () => {
|
||||
it('完整作答:Onboarding 选择能正确映射并生成画像', () => {
|
||||
const selections = {
|
||||
status: ['pregnant'],
|
||||
emotion: ['calm'],
|
||||
influence: ['work'],
|
||||
support: ['balance'],
|
||||
};
|
||||
|
||||
const answers = mapOnboardingSelectionsToQuestionnaireAnswers(selections);
|
||||
expect(answers).toEqual({
|
||||
mom_stage: 'expecting',
|
||||
emotion: 'calm',
|
||||
context: 'work',
|
||||
need: 'rest_balance',
|
||||
});
|
||||
|
||||
const p = buildUserProfileFromQuestionnaire(answers, {
|
||||
generatedAt: '2026-01-30T00:00:00Z',
|
||||
now: '2026-01-30T00:00:00Z',
|
||||
});
|
||||
|
||||
expect(p.stage).toEqual({ expecting: 1, parenting: 0, unknown: 0 });
|
||||
expect(p.emotion_score).toBe(0.8);
|
||||
expect(p.context).toEqual({ work: 1 });
|
||||
expect(p.need).toEqual({ rest_balance: 1 });
|
||||
expect(p.profile_answered).toEqual({ stage: true, emotion: true, context: true, need: true });
|
||||
});
|
||||
|
||||
it('全部跳过:仍能生成最小可计算画像(unknown=1)', () => {
|
||||
const answers = mapOnboardingSelectionsToQuestionnaireAnswers({});
|
||||
expect(answers).toEqual({ mom_stage: null, emotion: null, context: null, need: null });
|
||||
|
||||
const p = buildUserProfileFromQuestionnaire(answers, {
|
||||
generatedAt: '2026-01-30T00:00:00Z',
|
||||
now: '2026-01-30T00:00:00Z',
|
||||
});
|
||||
|
||||
expect(p.stage).toEqual({ unknown: 1 });
|
||||
expect(p.emotion_score).toBeNull();
|
||||
expect(p.context).toEqual({});
|
||||
expect(p.need).toEqual({});
|
||||
expect(p.profile_answered).toEqual({ stage: false, emotion: false, context: false, need: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ export type {
|
||||
UserProfileV1_2_Extended,
|
||||
} from './types';
|
||||
|
||||
export type { OnboardingSelections } from './onboardingMapping';
|
||||
|
||||
export {
|
||||
buildUserProfileFromQuestionnaire,
|
||||
computeProfileAnswered,
|
||||
@@ -13,3 +15,5 @@ export {
|
||||
normalizeAnswers,
|
||||
} from './scoring';
|
||||
|
||||
export { mapOnboardingSelectionsToQuestionnaireAnswers } from './onboardingMapping';
|
||||
|
||||
|
||||
61
client/src/features/userProfileScoring/onboardingMapping.ts
Normal file
61
client/src/features/userProfileScoring/onboardingMapping.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { QuestionnaireAnswersV1_2 } from './types';
|
||||
|
||||
/**
|
||||
* Onboarding UI 的选项 ID → 标准问卷枚举(可跳过)
|
||||
*
|
||||
* 说明:
|
||||
* - UI 侧每题目前是单选,但数据结构是 string[];这里取第 1 个作为答案
|
||||
* - 不存在错误处理:未知/非法值统一按“跳过”处理(返回 null)
|
||||
*/
|
||||
export type OnboardingSelections = Record<string, string[] | undefined>;
|
||||
|
||||
export function mapOnboardingSelectionsToQuestionnaireAnswers(
|
||||
selections: OnboardingSelections
|
||||
): QuestionnaireAnswersV1_2 {
|
||||
return {
|
||||
mom_stage: mapMomStage(selections.status?.[0]),
|
||||
emotion: mapEmotion(selections.emotion?.[0]),
|
||||
context: mapContext(selections.influence?.[0]),
|
||||
need: mapNeed(selections.support?.[0]),
|
||||
};
|
||||
}
|
||||
|
||||
function mapMomStage(raw: string | undefined): QuestionnaireAnswersV1_2['mom_stage'] {
|
||||
// 跳过:null(显式跳过)
|
||||
if (!raw) return null;
|
||||
// UI id → 标准枚举
|
||||
if (raw === 'pregnant') return 'expecting';
|
||||
if (raw === 'has_kids') return 'parenting';
|
||||
if (raw === 'no_fill') return 'unknown';
|
||||
// 其他非法值:按跳过处理
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapEmotion(raw: string | undefined): QuestionnaireAnswersV1_2['emotion'] {
|
||||
if (!raw) return null;
|
||||
// UI 当前选项:happy/calm/stressed/low
|
||||
if (raw === 'happy') return 'joyful';
|
||||
if (raw === 'calm') return 'calm';
|
||||
if (raw === 'stressed') return 'overwhelmed';
|
||||
if (raw === 'low') return 'low';
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapContext(raw: string | undefined): QuestionnaireAnswersV1_2['context'] {
|
||||
if (!raw) return null;
|
||||
// UI id 已与标准枚举一致:family/work/relationship/friends/health
|
||||
if (raw === 'family' || raw === 'work' || raw === 'relationship' || raw === 'friends' || raw === 'health') return raw;
|
||||
return null;
|
||||
}
|
||||
|
||||
function mapNeed(raw: string | undefined): QuestionnaireAnswersV1_2['need'] {
|
||||
if (!raw) return null;
|
||||
// UI id → 标准枚举
|
||||
if (raw === 'emotional') return 'emotional_support';
|
||||
if (raw === 'parenting') return 'parenting_pressure';
|
||||
if (raw === 'self_worth') return 'self_worth';
|
||||
if (raw === 'anxiety') return 'anxiety_relief';
|
||||
if (raw === 'balance') return 'rest_balance';
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user