69b34bd723
- 后端 ProfileSettingsV1 添加 DivinationTutorialSettings 字段 - 前端三个起卦页面添加首次访问检测,自动弹出教程 - 教程展示后更新 settings 标记,避免重复弹出 - 使用本地状态管理避免并发更新覆盖问题 - Agent 系统提示添加时间上下文信息
12 KiB
12 KiB
PRD: Divination Tutorial First-Visit Tracking
Background
用户首次进入起卦相关页面时,应该自动弹出教程引导。当前实现已有教程弹窗逻辑(用户可主动点击按钮触发),但缺少"首次访问自动弹出"的能力。
需要区分三个页面的首次访问状态:
- 起卦方式选择页面 (divination_screen.dart) - 显示手动起卦教程弹窗 (DivinationGuideDialog)
- 自动起卦页面 (auto_divination_screen.dart) - 显示 Onboarding 步骤引导
- 手动起卦页面 (manual_divination_screen.dart) - 显示 Onboarding 步骤引导
Goal
- 在用户 profile 的 settings 中添加字段,记录三个页面的教程是否已展示
- 前端在进入对应页面时检查 settings,首次访问自动弹出教程
- 教程展示后,前端调用后端 API 更新 settings 标记为已展示
- 老用户(settings 中没有这些字段)默认视为已看过教程,不弹出
Scope
- In scope:
- 后端: ProfileSettingsV1 schema 添加
divination_tutorial字段 - 后端: 更新 API 端点支持新字段
- 前端: ProfileSettingsV1 model 添加对应字段
- 前端: 三个起卦页面的首次访问检测和教程自动弹出逻辑
- 后端: ProfileSettingsV1 schema 添加
- Out of scope:
- 新增教程内容(复用现有教程逻辑)
- 用户主动触发教程的按钮(已存在,保持不变)
Current Architecture
前端现有教程逻辑
| 页面 | 文件路径 | 教程触发方式 |
|---|---|---|
| 起卦方式选择 | apps/lib/features/divination/presentation/screens/divination_screen.dart |
DivinationGuideDialog 弹窗,_showGuide() 函数 |
| 自动起卦 | apps/lib/features/divination/presentation/screens/auto_divination_screen.dart |
Onboarding 组件,_showGuide() 函数 |
| 手动起卦 | apps/lib/features/divination/presentation/screens/manual_divination_screen.dart |
Onboarding 组件,_showGuide() 函数 |
后端 Settings 结构
文件: backend/src/schemas/shared/user.py
class ProfileSettingsV1(BaseModel):
version: Literal[1] = 1
preferences: PreferenceSettings = Field(default_factory=PreferenceSettings)
privacy: dict[str, object] = Field(default_factory=dict)
notification: NotificationSettings = Field(default_factory=NotificationSettings)
前端 Settings Model
文件: apps/lib/features/settings/data/models/profile_settings.dart
class ProfileSettingsV1 {
final int version;
final String displayName;
final String bio;
final String? avatarPath;
final String? avatarUrl;
final PreferenceSettings preferences;
final Map<String, Object?> privacy;
final NotificationSettings notification;
}
更新 Settings API
- 端点:
PATCH /users/me/settings - Service:
backend/src/v1/users/service.py→update_settings() - 前端 API:
apps/lib/features/settings/data/apis/profile_api.dart→updateSettings()
Technical Design
1. 后端 Schema 变更
新增 DivinationTutorialSettings:
class DivinationTutorialSettings(BaseModel):
"""起卦教程首次访问状态"""
divination_entry_shown: bool = False # 起卦方式选择页教程已展示
auto_divination_shown: bool = False # 自动起卦页教程已展示
manual_divination_shown: bool = False # 手动起卦页教程已展示
修改 ProfileSettingsV1:
class ProfileSettingsV1(BaseModel):
version: Literal[1] = 1
preferences: PreferenceSettings = Field(default_factory=PreferenceSettings)
privacy: dict[str, object] = Field(default_factory=dict)
notification: NotificationSettings = Field(default_factory=NotificationSettings)
divination_tutorial: DivinationTutorialSettings = Field(default_factory=DivinationTutorialSettings)
2. 前端 Model 变更
新增 DivinationTutorialSettings:
class DivinationTutorialSettings {
const DivinationTutorialSettings({
this.divinationEntryShown = false,
this.autoDivinationShown = false,
this.manualDivinationShown = false,
});
final bool divinationEntryShown;
final bool autoDivinationShown;
final bool manualDivinationShown;
DivinationTutorialSettings copyWith({...});
}
修改 ProfileSettingsV1:
class ProfileSettingsV1 {
// ... existing fields ...
final DivinationTutorialSettings divinationTutorial;
ProfileSettingsV1 copyWith({
// ... existing params ...
DivinationTutorialSettings? divinationTutorial,
});
}
3. 前端页面逻辑变更
起卦方式选择页面 (divination_screen.dart):
// 在 initState 或 didChangeDependencies 中检查
void _checkFirstVisit() {
final settings = // 从 widget 或 context 获取 profileSettings
if (!settings.divinationTutorial.divinationEntryShown) {
// 延迟显示,确保页面渲染完成
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _showGuide(context, l10n);
});
// 调用 API 更新 settings
_markTutorialShown('divination_entry');
}
}
Future<void> _markTutorialShown(String tutorialKey) async {
// 调用 onProfileSettingsChanged 更新 settings
}
自动起卦页面 (auto_divination_screen.dart):
void _checkFirstVisit() {
final settings = // 获取 profileSettings
if (!settings.divinationTutorial.autoDivinationShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _showGuide();
});
_markTutorialShown('auto_divination');
}
}
手动起卦页面 (manual_divination_screen.dart):
void _checkFirstVisit() {
final settings = // 获取 profileSettings
if (!settings.divinationTutorial.manualDivinationShown) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _showGuide();
});
_markTutorialShown('manual_divination');
}
}
4. Settings 传递方式(已确认)
采用构造函数参数传入方式(选项 A)。
调用链分析:
EryaoApp (app.dart)
└── HomeScreen
├── profileSettings: ProfileSettingsV1
├── onProfileSettingsChanged: Future<void> Function(ProfileSettingsV1)
└── _onStartDivination()
└── DivinationScreen <-- 需要添加 profileSettings 和 onProfileSettingsChanged
├── _onStart() → AutoDivinationScreen <-- 需要添加
└── _onStart() → ManualDivinationScreen <-- 需要添加
需要修改的文件:
| 文件 | 修改内容 |
|---|---|
home_screen.dart:155-166 |
_onStartDivination() 传入 profileSettings 和 onProfileSettingsChanged |
divination_screen.dart |
构造函数添加 profileSettings 和 onProfileSettingsChanged;添加首次访问检测 |
divination_screen.dart:164-176 |
_onStart() 传入参数到 ManualDivinationScreen 和 AutoDivinationScreen |
auto_divination_screen.dart |
构造函数添加参数;添加首次访问检测 |
manual_divination_screen.dart |
构造函数添加参数;添加首次访问检测 |
5. 教程关闭回调处理
Onboarding 组件 (用于自动/手动起卦页面):
Onboarding组件有onEnd回调参数- 在
onEnd中调用_markTutorialShown()
DivinationGuideDialog (用于起卦方式选择页面):
- 这是一个
Dialog,关闭时Navigator.pop()被调用 - 需要在
showDialog的.then()中处理关闭回调 - 或者在弹窗显示后立即标记(因为用户已经看到了)
推荐方案: 在弹窗显示后立即标记,无需等待关闭
- 用户看到教程即视为"已展示"
- 避免因用户快速关闭导致 settings 未更新
6. 老用户兼容
- 后端新字段默认值为
false - 数据库中现有用户 settings 没有
divination_tutorial字段 - 前端解析时:
- 如果字段不存在,视为已看过教程(不弹出)
- 实现方式: JSON 解析时提供默认值
true,或检查字段是否存在
前端兼容逻辑:
factory DivinationTutorialSettings.fromJson(Map<String, dynamic>? json) {
if (json == null) {
// 老用户没有这个字段,默认全部 true(已看过)
return const DivinationTutorialSettings(
divinationEntryShown: true,
autoDivinationShown: true,
manualDivinationShown: true,
);
}
return DivinationTutorialSettings(
divinationEntryShown: json['divination_entry_shown'] ?? true,
autoDivinationShown: json['auto_divination_shown'] ?? true,
manualDivinationShown: json['manual_divination_shown'] ?? true,
);
}
6. API 更新流程
用户首次看到教程后:
- 前端调用
_markTutorialShown('auto_divination') - 构建
ProfileSettingsV1的 copyWith 更新对应字段 - 调用
onProfileSettingsChanged(updatedSettings) - 后端保存新 settings
注意: 只更新一个字段时,需要保留其他字段不变。
7. Onboarding 组件 onEnd 回调
查看 auto_divination_screen.dart:145-148:
return Onboarding(
key: _onboardingKey,
steps: steps,
onChanged: _onGuideStepChanged,
// 需要添加: onEnd: _markTutorialShown,
);
manual_divination_screen.dart 同理。
Implementation Checklist
后端
backend/src/schemas/shared/user.py添加DivinationTutorialSettings和修改ProfileSettingsV1- 确认 API 端点无需修改(使用现有的 PATCH + 完整 settings 替换)
- 单元测试: 新 schema 的序列化/反序列化
前端
apps/lib/features/settings/data/models/profile_settings.dart添加DivinationTutorialSettingsapps/lib/features/settings/data/apis/profile_api.dart更新_toSettings()解析逻辑- 确认三个页面如何获取
ProfileSettingsV1(需要调查) divination_screen.dart添加首次访问检测auto_divination_screen.dart添加首次访问检测manual_divination_screen.dart添加首次访问检测- 测试老用户兼容性(settings 无此字段时不弹出)
测试场景
- 新用户首次访问: 进入页面 → 自动弹出教程 → settings 更新为 shown
- 新用户再次访问: 进入页面 → 不弹出教程
- 老用户首次访问: settings 无此字段 → 不弹出教程(视为已看过)
- 用户主动触发: 点击教程按钮 → 正常弹出(无论是否首次)
Risks & Mitigations
| 风险 | 缓解措施 |
|---|---|
| 前端解析 settings 时字段缺失 | 使用 ?? true 默认值,老用户视为已看过 |
| 教程弹出时机过早导致 UI 闪烁 | 使用 addPostFrameCallback 延迟到下一帧 |
| API 调用失败导致 settings 未更新 | 不影响用户体验,下次访问仍会弹出(符合预期) |
| 并发更新 settings 可能覆盖其他字段 | 前端使用 copyWith 保留其他字段 |
Open Questions
Settings 传递方式: 已确认使用构造函数参数传入。教程关闭回调: 建议在弹窗显示后立即标记,无需等待关闭。
File Changes Summary
| 文件 | 类型 | 修改内容 |
|---|---|---|
backend/src/schemas/shared/user.py |
后端 | 添加 DivinationTutorialSettings 和修改 ProfileSettingsV1 |
apps/lib/features/settings/data/models/profile_settings.dart |
前端 | 添加 DivinationTutorialSettings 和修改 ProfileSettingsV1 |
apps/lib/features/settings/data/apis/profile_api.dart |
前端 | 更新 _toSettings() 解析新字段 |
apps/lib/features/home/presentation/screens/home_screen.dart |
前端 | _onStartDivination() 传入 profileSettings 和 onProfileSettingsChanged |
apps/lib/features/divination/presentation/screens/divination_screen.dart |
前端 | 添加构造函数参数 + 首次访问检测 + _markTutorialShown() |
apps/lib/features/divination/presentation/screens/auto_divination_screen.dart |
前端 | 添加构造函数参数 + 首次访问检测 + _markTutorialShown() |
apps/lib/features/divination/presentation/screens/manual_divination_screen.dart |
前端 | 添加构造函数参数 + 首次访问检测 + _markTutorialShown() |