新功能:个性化推荐算法
This commit is contained in:
@@ -5,7 +5,16 @@ import { OnboardingLayout } from '@/components/onboarding/OnboardingLayout';
|
||||
import { NameInputStep } from '@/components/onboarding/NameInputStep';
|
||||
import { SelectionStep } from '@/components/onboarding/SelectionStep';
|
||||
import { ReminderStep } from '@/components/onboarding/ReminderStep';
|
||||
import { setOnboardingCompleted, setUserProfile, setDailyReminderSettings } from '@/src/storage/appStorage';
|
||||
import { buildUserProfileFromQuestionnaire, mapOnboardingSelectionsToQuestionnaireAnswers } from '@/src/features/userProfileScoring';
|
||||
import { fetchRecoFeed } from '@/src/services/recoApi';
|
||||
import {
|
||||
recordRecoFeedServed,
|
||||
setOnboardingCompleted,
|
||||
setUserProfile,
|
||||
setDailyReminderSettings,
|
||||
setUserProfileScoring,
|
||||
setRecoFeedCache,
|
||||
} from '@/src/storage/appStorage';
|
||||
|
||||
const STEPS = [
|
||||
{ id: 'name', type: 'name', title: '我可以怎么称呼你?' },
|
||||
@@ -71,6 +80,40 @@ export default function OnboardingScreen() {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
const pushEnabled = status === 'granted';
|
||||
|
||||
// 将 Onboarding 选择映射为标准问卷枚举(允许跳过)
|
||||
const answers = mapOnboardingSelectionsToQuestionnaireAnswers(selections);
|
||||
|
||||
// 生成用户画像(供推荐/Push/Widget 复用)
|
||||
const scoringProfile = buildUserProfileFromQuestionnaire(answers);
|
||||
await setUserProfileScoring(scoringProfile);
|
||||
|
||||
// Onboarding 结束后预拉取一次 Feed 文案(失败不阻塞进入首页)
|
||||
try {
|
||||
const { items, meta } = await fetchRecoFeed({
|
||||
k: 30,
|
||||
user_profile: {
|
||||
profile_version: scoringProfile.profile_version,
|
||||
profile_source: scoringProfile.profile_source,
|
||||
profile_generated_at: scoringProfile.profile_generated_at,
|
||||
profile_confidence: scoringProfile.profile_confidence,
|
||||
profile_answered: scoringProfile.profile_answered,
|
||||
stage: scoringProfile.stage,
|
||||
emotion_score: scoringProfile.emotion_score,
|
||||
context: scoringProfile.context,
|
||||
need: scoringProfile.need,
|
||||
},
|
||||
});
|
||||
|
||||
await setRecoFeedCache({
|
||||
saved_at: new Date().toISOString(),
|
||||
items: items.map((x) => ({ content_id: x.content_id, text: x.text })),
|
||||
meta: meta as Record<string, unknown>,
|
||||
});
|
||||
await recordRecoFeedServed(items.map((x) => x.content_id));
|
||||
} catch {
|
||||
// 网络失败时使用首页本地 mock 兜底
|
||||
}
|
||||
|
||||
await setUserProfile({
|
||||
name,
|
||||
intents: Object.values(selections).flat()
|
||||
@@ -98,19 +141,30 @@ export default function OnboardingScreen() {
|
||||
};
|
||||
|
||||
const onSkip = () => {
|
||||
// 跳过整个 Onboarding:仍生成一个“全跳过”的最小画像,保证下游可用
|
||||
const scoringProfile = buildUserProfileFromQuestionnaire({});
|
||||
void setUserProfileScoring(scoringProfile);
|
||||
|
||||
// 标记已完成,避免下次启动再次进入 Onboarding
|
||||
void setOnboardingCompleted(true);
|
||||
|
||||
router.replace('/(app)/home');
|
||||
};
|
||||
|
||||
// 题目为单选:再次点击可取消;选择其他选项会替换为唯一选项
|
||||
const handleToggleSelection = (id: string) => {
|
||||
setSelections(prev => {
|
||||
const currentIds = prev[currentStep.id] || [];
|
||||
const nextIds = currentIds.includes(id)
|
||||
? currentIds.filter(i => i !== id)
|
||||
: [...currentIds, id];
|
||||
const nextIds = currentIds.includes(id) ? [] : [id];
|
||||
return { ...prev, [currentStep.id]: nextIds };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSkipStep = () => {
|
||||
setSelections((prev) => ({ ...prev, [currentStep.id]: [] }));
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<OnboardingLayout
|
||||
title={currentStep.title}
|
||||
@@ -134,6 +188,7 @@ export default function OnboardingScreen() {
|
||||
selectedIds={selections[currentStep.id] || []}
|
||||
onToggle={handleToggleSelection}
|
||||
onNext={onNext}
|
||||
onSkip={handleSkipStep}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user