Files
eryao/.trellis/tasks/04-17-feat-privacy-do-not-sell/prd.md
T
qzl 913ed26f8d feat(privacy): add personalized ads toggle in settings
- Add PrivacySettings schema with can_sell field (default: false)
- Move privacy toggle to GeneralSettingsScreen with clear labeling
- Update l10n for zh/en/zh_hant with user-friendly copy
- Clean up redundant PrivacyNotificationSettingsScreen
- Update profile-protocol.md to reflect new privacy schema
- Fix basedpyright warnings in user.py
2026-04-17 13:11:09 +08:00

191 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRD: Do Not Sell My Personal Information 开关
## 1. 功能需求概述
实现 CCPA/CPRA 合规的 "Do Not Sell My Personal Information" 功能:
- 隐私政策页添加 Do Not Sell 开关(胶囊状态栏切换)
- 设置 → 隐私页添加相同开关
- 后端存储用户隐私偏好
- 开关默认开启(用户选择不被销售)
## 2. 背景与合规要求
### CCPA/CPRA 要求
- 如果企业"销售"或"共享"个人信息用于广告,必须提供退出选项
- 即使声明不卖,仍需提供退出机制作为合规保险
- App Store 审核会检查此功能的存在
### 业务现状
- 隐私政策已声明 `WE DO NOT SELL YOUR PERSONAL INFORMATION`
- 但缺少用户端开关实现
## 3. 技术方案
### 3.1 后端实现
**存储位置**: `profiles.settings.privacy` (JSONB)
**设计决策**: 将 `privacy` 字段升级为结构化的 `PrivacySettings` 对象
**新增字段**:
```python
# backend/src/schemas/shared/user.py
class PrivacySettings(BaseModel):
"""隐私相关设置统一管理"""
do_not_sell: bool = True # 本次需求:Do Not Sell(默认 True
profile_visibility: str = "public" # 未来扩展:个人资料可见性
# ProfileSettingsV1 中替换
class ProfileSettingsV1(BaseModel):
privacy: PrivacySettings = Field(default_factory=PrivacySettings) # 替换 dict
```
**API 变更**: 复用现有 `PATCH /users/me/settings` 接口,无需新增 endpoint
**数据库迁移**: 无需 Schema 变更(privacy 是 JSONB,可自由扩展)
**向后兼容**:
- 现有 `privacy: dict` 数据会自动转换为 `PrivacySettings`
- 新字段使用默认值,不影响现有用户
### 3.2 Flutter 前端实现
**数据层**:
- 新增 `PrivacySettings` class(对应后端)
- `ProfileSettingsV1.privacy` 类型从 `Map<String, Object?>` 改为 `PrivacySettings`
- 默认值:`doNotSell = true`(默认开启)
**UI 实现**:
1. **主开关:隐私通知设置页** (`PrivacyNotificationSettingsScreen`)
- 在"隐私" section 下(最前面)添加 "Do Not Sell" 开关
- 复用 `SettingsSwitchTile` 组件(已有)
- 文案: "Limit Use of My Personal Information"
- 默认值: True(开启)
- 开关状态实时保存
- "Profile Visibility" 保持现状(占位符,显示 Coming Soon
2. **快捷入口:法律中心列表页** (`LegalCenterScreen`)
- 在"隐私" section 下(法律文档列表之后)添加独立按钮
- 不是开关,而是快捷入口(SettingsMenuTile
- 显示当前状态:已开启 / 已关闭
- 点击跳转到 `PrivacyNotificationSettingsScreen`
- 满足 CCPA/CPRA 隐私政策中提供"Do Not Sell"入口的要求
3. **隐私政策详情页** (`LegalDocumentScreen`)
- 保持现状:只显示静态 Markdown 内容
- **不添加开关**(避免重复功能,保持文档阅读纯粹性)
**代码结构**:
```dart
// apps/lib/features/settings/data/models/profile_settings.dart
class PrivacySettings {
const PrivacySettings({
this.doNotSell = true,
this.profileVisibility = 'public',
this.personalization = false,
this.historyVisibility = 'private',
});
final bool doNotSell;
final String profileVisibility;
final bool personalization;
final String historyVisibility;
PrivacySettings copyWith({...});
}
class ProfileSettingsV1 {
final PrivacySettings privacy; // 替换 Map<String, Object?>
// ...
}
```
### 3.3 国际化
需要新增以下 key(示例):
- `settingsDoNotSellTitle`: "Do Not Sell My Personal Information"
- `settingsDoNotSellDescription`: "Limit the use of your personal information"
- `settingsDoNotSellHint`: "When enabled, we will not sell or share your data"
## 4. 实现步骤
### Phase 1: 后端数据模型
- [ ] **Backend**: `schemas/shared/user.py` - 新增 `PrivacySettings` class
- 添加 `do_not_sell: bool = True`(本次需求)
- 添加未来扩展字段:`profile_visibility`, `personalization`, `history_visibility`
- [ ] **Backend**: `schemas/shared/user.py` - 更新 `ProfileSettingsV1`
-`privacy: dict[str, object]` 改为 `privacy: PrivacySettings`
- 添加 `parse_profile_settings` 兼容性逻辑(dict → PrivacySettings
### Phase 2: 前端数据模型
- [ ] **Flutter**: `settings/data/models/profile_settings.dart` - 新增 `PrivacySettings` class
- 包含 `doNotSell`, `profileVisibility`, `personalization`, `historyVisibility`
- 实现 `copyWith` 方法
- [ ] **Flutter**: `settings/data/models/profile_settings.dart` - 更新 `ProfileSettingsV1`
-`privacy` 类型从 `Map<String, Object?>` 改为 `PrivacySettings`
### Phase 3: UI 实现
- [ ] **Flutter**: 扩展 `PrivacyNotificationSettingsScreen`
- 在"隐私" section 最前面添加 Do Not Sell 开关(SettingsSwitchTile
- 保持 Profile Visibility 占位符不变
- [ ] **Flutter**: 扩展 `LegalCenterScreen`
- 添加快捷入口按钮(SettingsMenuTile
- 显示当前状态:已开启 / 已关闭
- 实现导航到 `PrivacyNotificationSettingsScreen`
### Phase 4: 业务逻辑
- [ ] 开关状态持久化(通过 `PATCH /users/me/settings`
- [ ] 法律中心快捷按钮显示当前状态(从 settings.privacy.doNotSell 读取)
### Phase 5: 国际化
- [ ] 添加中/英/繁三语 ARB keys
- `settingsDoNotSellTitle`: Do Not Sell My Personal Information
- `settingsDoNotSellDescription`: Limit use of your personal information
- `settingsDoNotSellEnabled`: Enabled
- `settingsDoNotSellDisabled`: Disabled
- [ ] 执行 `flutter gen-l10n`
## 5. 相关代码文件
### Backend
| 文件 | 说明 | 变更类型 |
|------|------|--------|
| `backend/src/schemas/shared/user.py` | 新增 PrivacySettings classProfileSettingsV1.privacy 改为 PrivacySettings 类型 | 修改 |
| `backend/src/v1/users/router.py` | 用户设置 API(复用 PATCH /me/settings | 无变更 |
| `backend/src/v1/users/service.py` | 设置更新逻辑 | 无变更 |
### Frontend
| 文件 | 说明 | 优先级 |
|------|------|--------|
| `apps/lib/features/settings/data/models/profile_settings.dart` | 新增 PrivacySettings classProfileSettingsV1.privacy 类型修改 | P0 |
| `apps/lib/features/settings/presentation/screens/privacy_notification_settings_screen.dart` | 添加 Do Not Sell 开关(主开关),保持 Profile Visibility 占位符 | P0 |
| `apps/lib/features/settings/presentation/screens/legal_center_screen.dart` | 添加快捷入口按钮 | P1 |
| `apps/lib/features/settings/presentation/widgets/settings_section_widgets.dart` | 复用 SettingsSwitchTile(已有) | - |
## 6. 测试计划
- [ ] 开关默认状态为开启
- [ ] 开关切换后值正确保存到后端
- [ ] App 重启后设置正确恢复
- [ ] 中/英/繁语言正确显示
## 7. 注意事项
1. **数据模型升级**: `privacy``Map` 升级为结构化 `PrivacySettings` 对象
2. **向后兼容**: 后端需要兼容现有 `privacy: dict` 数据的读取
3. **合规文案**: 使用 "Limit Use" 而非 "We Don't Sell",避免直接承诺
4. **默认值**: 默认开启(opt-out),符合隐私保护趋势
5. **开关位置**: 主开关在隐私设置页,法律中心只提供快捷入口(避免重复)
6. **开关形式**: 使用 `SettingsSwitchTile` 组件(已有)
7. **法律中心实现**: 添加快捷按钮(SettingsMenuTile),显示当前状态,点击跳转
8. **后端兼容性**: 由于使用 JSONB 扩展,无需数据库迁移
9. **状态同步**: 确保法律中心快捷按钮显示的状态与实际设置一致
10. **Profile Visibility**: 保持现状(占位符 + Coming Soon),不影响本次需求