From 9bc24fa0c43006e326372de12c8f33bf7e1757c4 Mon Sep 17 00:00:00 2001 From: ZL-Q Date: Tue, 28 Apr 2026 17:17:38 +0800 Subject: [PATCH] chore(task): archive 04-28-feat-points-ledger --- .../IMPLEMENTATION_PLAN.md | 8 +- .../tasks/04-27-feat-ios-apple-pay/prd.md | 18 +- .../IMPLEMENTATION_PLAN.md | 404 ++++++++++++++++++ .../prd.md | 106 +++++ .../task.json | 53 +++ .../IMPLEMENTATION_PLAN.md | 294 +++++++++++++ .../04-28-refactor-unify-language/prd.md | 93 ++++ .../IMPLEMENTATION_PLAN.md | 334 +++++++++++++++ .../2026-04/04-28-feat-points-ledger/prd.md | 201 +++++++++ .../04-28-feat-points-ledger/task.json | 85 ++++ 10 files changed, 1583 insertions(+), 13 deletions(-) create mode 100644 .trellis/tasks/04-28-feat-locale-timezone-bootstrap/IMPLEMENTATION_PLAN.md create mode 100644 .trellis/tasks/04-28-feat-locale-timezone-bootstrap/prd.md create mode 100644 .trellis/tasks/04-28-feat-locale-timezone-bootstrap/task.json create mode 100644 .trellis/tasks/04-28-refactor-unify-language/IMPLEMENTATION_PLAN.md create mode 100644 .trellis/tasks/04-28-refactor-unify-language/prd.md create mode 100644 .trellis/tasks/archive/2026-04/04-28-feat-points-ledger/IMPLEMENTATION_PLAN.md create mode 100644 .trellis/tasks/archive/2026-04/04-28-feat-points-ledger/prd.md create mode 100644 .trellis/tasks/archive/2026-04/04-28-feat-points-ledger/task.json diff --git a/.trellis/tasks/04-27-feat-ios-apple-pay/IMPLEMENTATION_PLAN.md b/.trellis/tasks/04-27-feat-ios-apple-pay/IMPLEMENTATION_PLAN.md index ec6edc1..959ac40 100644 --- a/.trellis/tasks/04-27-feat-ios-apple-pay/IMPLEMENTATION_PLAN.md +++ b/.trellis/tasks/04-27-feat-ios-apple-pay/IMPLEMENTATION_PLAN.md @@ -5,7 +5,7 @@ ### 已完成 - [x] 协议文档更新(`user-points-chat-data-protocol.md`、`http-error-codes.md`) - [x] PRD 退款扣回策略已明确 -- [x] 套餐配置 YAML 已正确命名(`new_user_pack`, `basic_pack` 等) +- [x] 套餐配置 YAML 已正确命名(`new_user_pack`, `starter_pack` 等) - [x] **Phase 1: 数据库与枚举**(2026-04-27 完成) - [x] **Phase 2: 后端支付服务**(2026-04-27 完成) - [x] **Phase 3: iOS / Flutter IAP 接入**(2026-04-27 完成) @@ -178,8 +178,8 @@ product_mappings: app_store_product_id: com.meeyao.qianwen.new_user_pack credits: 60 type: starter - basic_pack: - app_store_product_id: com.meeyao.qianwen.basic_pack + starter_pack: + app_store_product_id: com.meeyao.qianwen.starter_pack credits: 100 type: regular popular_pack: @@ -286,7 +286,7 @@ apps/lib/features/payments/ - [x] 创建 4 个消耗型 IAP 商品(Product ID 已确认与映射表一致) - [x] Product ID 与映射表一致 - `com.meeyao.qianwen.new_user_pack` — 新手包 - - `com.meeyao.qianwen.basic_pack` — 基础包 + - `com.meeyao.qianwen.starter_pack` — 入门包 - `com.meeyao.qianwen.popular_pack` — 热门包 - `com.meeyao.qianwen.premium_pack` — 高级包 - [ ] 配置价格和描述 diff --git a/.trellis/tasks/04-27-feat-ios-apple-pay/prd.md b/.trellis/tasks/04-27-feat-ios-apple-pay/prd.md index 342647b..f5363f0 100644 --- a/.trellis/tasks/04-27-feat-ios-apple-pay/prd.md +++ b/.trellis/tasks/04-27-feat-ios-apple-pay/prd.md @@ -15,7 +15,7 @@ - 成本/审计流水:`points_audit_ledger` - 注册奖励与新手包资格表:`register_bonus_claims` - 积分套餐配置:`backend/src/core/config/static/packages/*.yaml` -- 当前套餐:`new_user_pack`、`basic_pack`、`popular_pack`、`premium_pack` +- 当前套餐:`new_user_pack`、`starter_pack`、`popular_pack`、`premium_pack` - 当前套餐接口:`GET /api/v1/points/packages` - 当前积分余额接口:`GET /api/v1/points/balance` @@ -62,7 +62,7 @@ | 后端 `product_code` | App Store Product ID | 类型 | 积分 | 备注 | |---|---|---|---:|---| | `new_user_pack` | `com.meeyao.qianwen.new_user_pack` | starter | 60 | 每个邮箱身份只允许购买一次 | -| `basic_pack` | `com.meeyao.qianwen.basic_pack` | regular | 100 | 可重复购买 | +| `starter_pack` | `com.meeyao.qianwen.starter_pack` | regular | 100 | 可重复购买 | | `popular_pack` | `com.meeyao.qianwen.popular_pack` | regular | 210 | 可重复购买 | | `premium_pack` | `com.meeyao.qianwen.premium_pack` | regular | 415 | 可重复购买 | @@ -109,7 +109,7 @@ |---|---|---| | `id` | UUID PK | 内部交易记录 ID | | `user_id` | UUID not null | 当前购买归属用户,来自后端 JWT,不接受客户端传入 | -| `product_code` | varchar not null | 后端套餐码,例如 `basic_pack` | +| `product_code` | varchar not null | 后端套餐码,例如 `starter_pack` | | `app_store_product_id` | varchar not null | Apple 商品 ID | | `transaction_id` | varchar not null unique | Apple 交易 ID,核心幂等键 | | `original_transaction_id` | varchar null | Apple 原始交易 ID | @@ -165,8 +165,8 @@ { "source": "apple_iap", "platform": "ios", - "product_code": "basic_pack", - "app_store_product_id": "com.meeyao.qianwen.basic_pack", + "product_code": "starter_pack", + "app_store_product_id": "com.meeyao.qianwen.starter_pack", "transaction_id": "1000000123456789", "original_transaction_id": "1000000123456789", "environment": "Production", @@ -209,8 +209,8 @@ ```json { - "productCode": "basic_pack", - "appStoreProductId": "com.meeyao.qianwen.basic_pack", + "productCode": "starter_pack", + "appStoreProductId": "com.meeyao.qianwen.starter_pack", "transactionId": "1000000123456789", "signedTransactionInfo": "eyJhbGciOiJFUzI1NiIs...", "appAccountToken": "7c4c7a82-2f6f-4e70-b57a-8b0a7f2e9b72" @@ -230,7 +230,7 @@ ```json { "status": "granted", - "productCode": "basic_pack", + "productCode": "starter_pack", "transactionId": "1000000123456789", "creditsAdded": 100, "newBalance": 180, @@ -243,7 +243,7 @@ ```json { "status": "already_granted", - "productCode": "basic_pack", + "productCode": "starter_pack", "transactionId": "1000000123456789", "creditsAdded": 0, "newBalance": 180, diff --git a/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/IMPLEMENTATION_PLAN.md b/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..e5f3e6b --- /dev/null +++ b/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/IMPLEMENTATION_PLAN.md @@ -0,0 +1,404 @@ +# Implementation Plan: App启动时语言和时区自动设置 + +## Phase 1: 前端 - 读取系统语言/时区 + +### 1.1 创建系统Locale工具函数 + +**文件**: `apps/lib/app/locale_utils.dart` (新建) + +**Locale映射规则** (基于项目现有约定): + +| 系统Locale | Flutter Locale | 存储Tag | +|-----------|----------------|---------| +| en, en-US, en-GB, ... | `Locale('en')` | `en-US` | +| zh, zh-CN, zh-SG, zh-Hans-* | `Locale('zh')` | `zh-CN` | +| zh-Hant, zh-TW, zh-HK, zh-MO | `Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant')` | `zh-Hant` | +| 其他 | null (回退默认) | `zh-CN` | + +```dart +import 'dart:ui'; +import 'package:flutter/material.dart'; + +/// 从系统Locale映射到App支持的Locale +/// 返回 null 表示不支持,调用方应使用默认值 +Locale? resolveSystemLocale(Locale systemLocale) { + final lang = systemLocale.languageCode.toLowerCase(); + final script = systemLocale.scriptCode; + final country = systemLocale.countryCode; + + // 英文: en, en-US, en-GB, ... → Locale('en') → 存储 en-US + if (lang == 'en') { + return const Locale('en'); + } + + // 中文处理 + if (lang == 'zh') { + // 繁体: zh-Hant, zh-TW, zh-HK, zh-MO → Locale(zh, Hant) → 存储 zh-Hant + if (script == 'Hant' || country == 'TW' || country == 'HK' || country == 'MO') { + return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'); + } + // 简体: zh, zh-CN, zh-SG, zh-Hans-* → Locale('zh') → 存储 zh-CN + return const Locale('zh'); + } + + // 其他语言不支持,回退到默认 + return null; +} + +/// 获取系统首选Locale +Locale getSystemLocale() { + return PlatformDispatcher.instance.locale; +} +``` + +### 1.2 创建系统时区工具函数 + +**文件**: `apps/lib/app/timezone_utils.dart` (新建) + +**方案选择**: +- 方案1: 使用 `flutter_timezone` 包 (推荐,直接返回IANA ID) +- 方案2: 使用 `DateTime.now().timeZoneName` + 映射表 (无额外依赖,但需维护映射) + +```dart +import 'package:flutter_timezone/flutter_timezone.dart'; + +/// 获取系统时区IANA ID +/// 例如: Asia/Shanghai, America/New_York, Europe/London +Future getSystemTimezone() async { + try { + final timezone = await FlutterTimezone.getLocalTimezone(); + // 验证是否为有效IANA ID + if (timezone.isNotEmpty) { + return timezone; + } + } catch (_) { + // ignore + } + // 回退到默认 + return 'Asia/Shanghai'; +} +``` + +**依赖添加** (`apps/pubspec.yaml`): +```yaml +dependencies: + flutter_timezone: ^3.0.1 +``` + +### 1.3 扩展SessionStore + +**文件**: `apps/lib/core/auth/session_store.dart` + +```dart +// 新增 +static const String _timezoneKey = 'selected_timezone'; + +Future saveTimezone(String timezone) async { + await _kvStore.setString(_timezoneKey, timezone); +} + +Future getTimezone() async { + return _kvStore.getString(_timezoneKey); +} +``` + +### 1.4 修改App启动流程 + +**文件**: `apps/lib/app/app.dart` + +**步骤1**: 添加 `_timezone` 状态变量声明 + +```dart +// 在 State 类顶部添加 +String _timezone = 'Asia/Shanghai'; +``` + +**步骤2**: 修改 `_bootstrap()` 方法 +```dart +Future _bootstrap() async { + // 1. 语言处理 + final savedLocaleTag = await _sessionStore.getLocaleTag(); + final Locale locale; + if (savedLocaleTag != null) { + locale = localeFromLanguageTag(savedLocaleTag); + } else { + final systemLocale = getSystemLocale(); + locale = resolveSystemLocale(systemLocale) ?? const Locale('zh'); + await _sessionStore.saveLocaleTag(languageTagFromLocale(locale)); + } + + // 2. 时区处理 + final savedTimezone = await _sessionStore.getTimezone(); + final String timezone; + if (savedTimezone != null) { + timezone = savedTimezone; + } else { + timezone = await getSystemTimezone(); + await _sessionStore.saveTimezone(timezone); + } + + // 3. 设置状态 + setState(() { + _locale = locale; + _timezone = timezone; + }); + + // 4. 启动认证 + await _authBloc.start(); +} +``` + +--- + +## Phase 2: 前端 - 注册时传递语言/时区 + +### 2.1 扩展AuthApi + +**文件**: `apps/lib/features/auth/data/apis/auth_api.dart` + +```dart +Future createEmailSession({ + required String email, + required String token, + String? language, + String? timezone, +}) async { + final data = { + 'email': email, + 'token': token, + }; + if (language != null) data['language'] = language; + if (timezone != null) data['timezone'] = timezone; + + final json = await _apiClient.postJson('/api/v1/auth/email-session', data: data); + return SessionResponse.fromJson(json); +} +``` + +### 2.2 扩展AuthRepository + +**文件**: `apps/lib/features/auth/data/repositories/auth_repository.dart` + +```dart +// 接口定义 +abstract class AuthRepository { + // ... 现有方法 ... + Future loginWithEmailOtp({ + required String email, + required String otp, + String? language, + String? timezone, + }); +} + +// 实现 +class AuthRepositoryImpl implements AuthRepository { + // ... 现有代码 ... + + @override + Future loginWithEmailOtp({ + required String email, + required String otp, + String? language, + String? timezone, + }) async { + final response = await _authApi.createEmailSession( + email: email, + token: otp, + language: language, + timezone: timezone, + ); + // ... 现有的 session 处理逻辑 ... + } +} +``` + +### 2.3 扩展AuthBloc + +**文件**: `apps/lib/features/auth/presentation/bloc/auth_bloc.dart` + +```dart +Future loginWithOtp({ + required String email, + required String otp, + String? language, + String? timezone, +}) async { + final user = await _repository.loginWithEmailOtp( + email: email, + otp: otp, + language: language, + timezone: timezone, + ); + // ... +} +``` + +### 2.4 修改App调用点 + +**文件**: `apps/lib/app/app.dart` + +```dart +// LoginScreen 的 onLoginWithOtp 回调 +onLoginWithOtp: (email, otp) { + return _authBloc.loginWithOtp( + email: email, + otp: otp, + language: languageTagFromLocale(_locale), + timezone: _timezone, + ); +}, +``` + +--- + +## Phase 3: 后端 - 接收语言/时区 + +### 3.1 扩展Schema + +**文件**: `backend/src/v1/auth/schemas.py` + +```python +class EmailSessionCreateRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + + email: str = Field(pattern=SUPABASE_EMAIL_PATTERN) + token: str = Field(min_length=6, max_length=6) + language: str | None = Field(default=None, max_length=20) + timezone: str | None = Field(default=None, max_length=50) +``` + +### 3.2 扩展AuthService + +**文件**: `backend/src/v1/auth/service.py` + +```python +async def create_email_session( + self, + payload: EmailSessionCreateRequest, +) -> SessionResponse: + # ... 现有的 session 创建逻辑 ... + + # 如果提供了语言/时区,更新Profile + if payload.language or payload.timezone: + await self._update_profile_preferences( + user_id=user.id, + language=payload.language, + timezone=payload.timezone, + ) + + return result + +async def _update_profile_preferences( + self, + user_id: UUID, + language: str | None, + timezone: str | None, +) -> None: + """更新用户Profile的语言/时区偏好设置""" + profile = await self.profile_repository.get_profile_by_user_id(user_id) + if profile is None: + return + + settings = profile.settings or {} + preferences = settings.get("preferences", {}) + + if language is not None: + preferences["language"] = language + if timezone is not None: + preferences["timezone"] = timezone + + settings["preferences"] = preferences + profile.settings = settings + + await self.profile_repository.save() +``` + +### 3.3 AuthService依赖注入 + +**文件**: `backend/src/v1/auth/service.py` + +AuthService 需要注入 `ProfileRepository` 或 `UserRepository` 以便更新 Profile。 + +```python +@dataclass +class AuthService: + # ... 现有依赖 ... + profile_repository: SQLAlchemyUserRepository # 新增 +``` + +**或**: 复用现有的 `v1/users/service.py` 中的 `UserService.update_settings()` 方法。 + +--- + +## Phase 4: 前端 - 登录后同步时区 + +### 4.1 修改_refreshProfile + +**文件**: `apps/lib/app/app.dart` + +```dart +Future _refreshProfile({required String userEmail}) async { + // ... 现有逻辑 ... + + final serverLanguage = profile.preferences.language; + final serverTimezone = profile.preferences.timezone; + + // 同步语言 + await _sessionStore.saveLocaleTag(serverLanguage); + + // 同步时区 + await _sessionStore.saveTimezone(serverTimezone); + + setState(() { + _locale = localeFromLanguageTag(serverLanguage); + _timezone = serverTimezone; + _profileSettings = profile; + // ... + }); +} +``` + +--- + +## Phase 5: 验证和测试 + +### 5.1 单元测试 + +- [ ] `resolveSystemLocale` 映射正确性 +- [ ] `getSystemTimezone` 返回有效IANA ID +- [ ] `SessionStore` 时区存取 + +### 5.2 集成测试 + +- [ ] 新设备首次打开 → 系统语言/时区生效 +- [ ] 新用户注册 → 后端Profile正确 +- [ ] 已有用户登录 → 服务器值同步 +- [ ] 有本地存储 → 使用本地值 + +### 5.3 手动测试 + +- [ ] iOS系统语言设为英文 → App显示英文 +- [ ] iOS系统语言设为繁体中文 → App显示繁体 +- [ ] iOS系统时区设为非上海 → App时区正确 + +--- + +## 实施顺序 + +1. **Phase 1.3**: 扩展SessionStore(最小改动,无依赖) +2. **Phase 1.1-1.2**: 创建工具函数 +3. **Phase 1.4**: 修改启动流程 +4. **Phase 3.1**: 后端Schema扩展 +5. **Phase 3.2-3.3**: 后端Service逻辑 +6. **Phase 2**: 前端注册流程扩展 +7. **Phase 4**: 登录后同步时区 +8. **Phase 5**: 测试验证 + +--- + +## 依赖 + +- 可选: `flutter_timezone` 包(如需精确时区ID) + - 添加到 `apps/pubspec.yaml` + - 或使用 `DateTime.now().timeZoneName` + 映射表(无依赖) diff --git a/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/prd.md b/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/prd.md new file mode 100644 index 0000000..cc51912 --- /dev/null +++ b/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/prd.md @@ -0,0 +1,106 @@ +# PRD: App启动时语言和时区自动设置 + +## 背景 + +当前App启动时语言硬编码为 `zh`,不读取iOS系统语言;时区完全不处理。需要实现三种场景的自动设置逻辑。 + +## 需求 + +### 场景1:新用户首次打开App → 注册时写入后端 + +1. App无本地存储信息(首次打开) +2. 读取iOS系统语言和时区 +3. 自动设置App界面语言和时区 +4. 用户注册时,将语言和时区作为请求参数传给后端 +5. 后端创建用户Profile时使用传入值 + +### 场景2:已有用户在新设备首次登录 → 从服务器同步 + +1. App无本地存储信息(新设备首次打开) +2. 读取iOS系统语言和时区作为临时值 +3. 自动设置App界面语言和时区(临时) +4. 用户登录成功后,从服务器拉取Profile中的语言和时区 +5. 用服务器值更新本地存储和App设置 + +### 场景3:有本地存储 → 使用本地值 + +1. App有本地存储的语言和时区信息 +2. 直接使用本地值,不读取系统语言和时区 +3. 登录后仍从服务器同步(确保一致性) + +## 技术方案 + +### 前端 + +1. **读取系统语言** + - 使用 `PlatformDispatcher.instance.locale` 获取系统首选语言 + - 通过 `resolveSystemLocale()` 映射到 App 支持的 Locale + +2. **读取系统时区** + - 使用 `flutter_timezone` 包的 `getLocalTimezone()` 获取 IANA 时区 ID + - 直接使用返回值,后端会验证有效性 + +3. **本地存储扩展** + - `SessionStore` 新增 `saveTimezone()` / `getTimezone()` + - 现有 `saveLocaleTag()` / `getLocaleTag()` 已支持语言 + +4. **启动流程修改** (`app.dart:_bootstrap`) + ``` + if (本地有locale) { + 使用本地locale + } else { + 读取系统locale → 保存到本地 + } + + if (本地有时区) { + 使用本地时区 + } else { + 读取系统时区 → 保存到本地 + } + ``` + +5. **注册请求扩展** + - `AuthApi.createEmailSession` 新增 `language` 和 `timezone` 参数 + - `EmailSessionCreateRequest` schema 扩展 + +6. **登录后同步** + - 现有 `_refreshProfile()` 已同步语言 + - 扩展同步时区 + +### 后端 + +1. **Schema扩展** (`v1/auth/schemas.py`) + ```python + class EmailSessionCreateRequest(BaseModel): + email: str + token: str + language: str | None = None # BCP-47 tag + timezone: str | None = None # IANA timezone + ``` + +2. **Service逻辑** (`v1/auth/service.py`) + - `create_email_session` 接收语言/时区 + - 创建/更新Profile时使用传入值(如果提供) + +3. **Profile默认值保持不变** + - `language: str = "zh-CN"` + - `timezone: str = "Asia/Shanghai"` + +## 验收标准 + +1. 新设备首次打开App,界面语言与iOS系统语言一致 +2. 新用户注册后,后端Profile的语言/时区与App一致 +3. 已有用户登录后,App语言/时区与服务器Profile一致 +4. 有本地存储时,App启动不请求系统语言/时区 +5. 支持的语言映射: + - iOS `en` / `en-US` / `en-GB` → Flutter `Locale('en')` → 存储 `en-US` + - iOS `zh` / `zh-CN` / `zh-SG` / `zh-Hans-*` → Flutter `Locale('zh')` → 存储 `zh-CN` + - iOS `zh-Hant` / `zh-TW` / `zh-HK` / `zh-MO` → Flutter `Locale(zh, Hant)` → 存储 `zh-Hant` +6. 时区为有效IANA ID (如 `Asia/Shanghai`, `America/New_York`) + +## 风险 + +1. iOS系统语言可能不在支持列表中 → 回退到 `zh-CN` +2. `flutter_timezone` 包可能返回无效时区 → 后端验证 + 前端回退默认值 +3. 用户修改系统语言/时区后,App不会自动更新(符合场景3设计) +4. 新用户注册时网络请求失败 → Profile 使用后端默认值,用户可在设置中修改 diff --git a/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/task.json b/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/task.json new file mode 100644 index 0000000..3759293 --- /dev/null +++ b/.trellis/tasks/04-28-feat-locale-timezone-bootstrap/task.json @@ -0,0 +1,53 @@ +{ + "title": "App启动时语言和时区自动设置", + "slug": "04-28-feat-locale-timezone-bootstrap", + "status": "pending", + "created_at": "2026-04-28", + "phases": [ + { + "name": "Phase 1: 前端读取系统语言/时区", + "status": "pending", + "items": [ + "1.1 创建系统Locale工具函数", + "1.2 创建系统时区工具函数", + "1.3 扩展SessionStore存储时区", + "1.4 修改App启动流程_bootstrap" + ] + }, + { + "name": "Phase 2: 前端注册时传递语言/时区", + "status": "pending", + "items": [ + "2.1 扩展AuthApi.createEmailSession", + "2.2 扩展AuthRepository", + "2.3 扩展AuthBloc.loginWithOtp", + "2.4 修改App调用点传递语言/时区" + ] + }, + { + "name": "Phase 3: 后端接收语言/时区", + "status": "pending", + "items": [ + "3.1 扩展EmailSessionCreateRequest Schema", + "3.2 扩展AuthService.create_email_session", + "3.3 新增Profile更新方法" + ] + }, + { + "name": "Phase 4: 前端登录后同步时区", + "status": "pending", + "items": [ + "4.1 修改_refreshProfile同步时区" + ] + }, + { + "name": "Phase 5: 测试验证", + "status": "pending", + "items": [ + "5.1 单元测试", + "5.2 集成测试", + "5.3 手动测试" + ] + } + ] +} diff --git a/.trellis/tasks/04-28-refactor-unify-language/IMPLEMENTATION_PLAN.md b/.trellis/tasks/04-28-refactor-unify-language/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..628c77b --- /dev/null +++ b/.trellis/tasks/04-28-refactor-unify-language/IMPLEMENTATION_PLAN.md @@ -0,0 +1,294 @@ +# IMPLEMENTATION_PLAN:统一语言设置 + +## 前置条件 + +| 条件 | 状态 | +|------|------| +| 后端 Schema 定义清晰 | ✅ | +| 前端 Model 定义清晰 | ✅ | +| AI 运行时使用 `ai_language` | ✅ | +| 协议文档存在 | ✅ | + +## 实现步骤 + +### Step 1: 更新协议文档 + +**文件**: `docs/protocols/profile/profile-protocol.md` + +- 将 `interface_language` 和 `ai_language` 合并为 `language` +- 更新示例 JSON + +--- + +### Step 2: 后端 Schema + +**文件**: `backend/src/schemas/shared/user.py` + +```python +class PreferenceSettings(BaseModel): + language: str = "zh-CN" + timezone: str = "Asia/Shanghai" + country: str = "US" + + @field_validator("language") + @classmethod + def validate_language(cls, value: str) -> str: + if not _BCP47_PATTERN.fullmatch(value): + raise ValueError("language must be a valid BCP-47 tag") + return value +``` + +--- + +### Step 3: 后端 AI 运行时 + +**文件**: `backend/src/core/agentscope/runtime/runner.py` + +```python +# 第 268-276 行 +language = "zh-CN" +if user_context.settings is not None: + prefs = getattr(user_context.settings, "preferences", None) + if prefs is not None: + language = getattr(prefs, "language", "zh-CN") or "zh-CN" + +system_prompt = build_system_prompt( + agent_type=stage_config.agent_type, + language=language, + llm_config=stage_config.llm_config, + tools=None, + now_utc=datetime.now(timezone.utc), +) +``` + +--- + +### Step 4: 后端 Prompt 构建 + +**文件**: `backend/src/core/agentscope/prompts/system_prompt.py` + +```python +def _build_output_rules(*, language: str) -> str: + lang_label = _get_language_label(language) + ... + +def build_system_prompt( + *, + agent_type: AgentType, + language: str, + llm_config: LlmConfig, + tools: list[ToolSchema] | None, + now_utc: datetime, +) -> str: + ... + _build_output_rules(language=language), +``` + +**文件**: `backend/src/core/agentscope/prompts/worker_rules.py` + +```python +def get_worker_role_playing(language: str) -> str: + _ = language + ... + +def get_worker_output_rules(language: str) -> str: + if language.startswith("en"): + ... +``` + +**文件**: `backend/src/core/agentscope/prompts/agent_prompt.py` + +```python +def build_agent_prompt( + *, + language: str = "zh-CN", +) -> str: + role_playing = get_worker_role_playing(language) + output_rules = get_worker_output_rules(language) + ... +``` + +--- + +### Step 5: 后端测试 + +**文件**: `backend/tests/unit/test_parse_profile_settings.py` + +- 字段名 `ai_language` → `language` +- 字段名 `interface_language` → `language` + +**文件**: `backend/tests/unit/test_agentscope_prompts.py` + +- 参数名 `ai_language` → `language` + +--- + +### Step 6: 数据库数据更新 + +直接用 Supabase MCP 执行 SQL(无需迁移脚本): + +```sql +UPDATE profiles +SET settings = jsonb_set( + settings - 'interface_language' - 'ai_language', + '{preferences,language}', + COALESCE( + settings->'preferences'->>'interface_language', + settings->'preferences'->>'ai_language', + '"zh-CN"' + )::jsonb +) +WHERE settings->'preferences' ?| array['interface_language', 'ai_language']; +``` + +--- + +### Step 7: 前端 Model + +**文件**: `apps/lib/features/settings/data/models/profile_settings.dart` + +```dart +class PreferenceSettings { + const PreferenceSettings({ + this.language = 'zh-CN', + this.timezone = 'Asia/Shanghai', + this.country = 'US', + }); + + final String language; + final String timezone; + final String country; + + PreferenceSettings copyWith({ + String? language, + String? timezone, + String? country, + }) { + return PreferenceSettings( + language: language ?? this.language, + timezone: timezone ?? this.timezone, + country: country ?? this.country, + ); + } +} + +// 更新 defaultsForLocale +factory ProfileSettingsV1.defaultsForLocale(Locale locale) { + final tag = languageTagFromLocale(locale); + return ProfileSettingsV1( + preferences: PreferenceSettings(language: tag), + ); +} +``` + +--- + +### Step 8: 前端 API + +**文件**: `apps/lib/features/settings/data/apis/profile_api.dart` + +```dart +// 序列化 (第 45 行) +'language': settings.preferences.language, + +// 反序列化 (第 114 行) +language: (preferencesRaw['language'] as String?) ?? 'zh-CN', +``` + +--- + +### Step 9: 前端设置界面 + +**文件**: `apps/lib/features/settings/presentation/screens/general_settings_screen.dart` + +移除第 75-95 行的 AI 语言 `SettingsMenuTile`,修改剩余的语言选项: + +```dart +SettingsMenuTile( + icon: Icons.language_rounded, + title: l10n.settingsLanguage, + subtitle: displayLanguageLabel( + l10n, + _settings.preferences.language, + ), + tint: colors.primary, + background: colors.surfaceContainerHighest, + showDivider: false, + onTap: () => _selectLanguage( + _settings.preferences.language, + (lang) => setState(() { + _settings = _settings.copyWith( + preferences: _settings.preferences.copyWith( + language: lang, + ), + ); + }), + ), +), +``` + +--- + +### Step 10: 前端 i18n + +**文件**: `apps/lib/l10n/app_zh.arb` + +```diff +- "settingsInterfaceLanguage": "界面语言", +- "settingsAiLanguage": "AI回复语言", +- "settingsAiLanguageHint": "该字段将对齐..." ++ "settingsLanguage": "语言", +``` + +**文件**: `apps/lib/l10n/app_en.arb` + +```diff +- "settingsInterfaceLanguage": "Interface Language", +- "settingsAiLanguage": "AI Response Language", +- "settingsAiLanguageHint": "This field will align..." ++ "settingsLanguage": "Language", +``` + +**文件**: `apps/lib/l10n/app_zh_hant.arb` + +```diff +- "settingsInterfaceLanguage": "介面語言", +- "settingsAiLanguage": "AI 回覆語言", ++ "settingsLanguage": "語言", +``` + +--- + +### Step 11: 重新生成 l10n + +```bash +cd apps && flutter gen-l10n +``` + +--- + +### Step 12: 更新其他协议文档 + +**文件**: `docs/protocols/divination/divination-run-protocol.md` + +第 240 行: +```diff +- - Language rule: `conclusion`, `focus_points`, `advice`, `keywords`, `answer` should follow user `ai_language` preference unless user explicitly requests otherwise. ++ - Language rule: `conclusion`, `focus_points`, `advice`, `keywords`, `answer` should follow user `language` preference unless user explicitly requests otherwise. +``` + +--- + +## 验证 + +```bash +# 后端 +cd backend +uv run pytest tests/unit/test_parse_profile_settings.py tests/unit/test_agentscope_prompts.py -v +./infra/scripts/dev-migrate.sh migrate + +# 前端 +cd apps +flutter gen-l10n +flutter analyze +``` diff --git a/.trellis/tasks/04-28-refactor-unify-language/prd.md b/.trellis/tasks/04-28-refactor-unify-language/prd.md new file mode 100644 index 0000000..39ee361 --- /dev/null +++ b/.trellis/tasks/04-28-refactor-unify-language/prd.md @@ -0,0 +1,93 @@ +# PRD:统一语言设置 + +## 1. 背景 + +当前存在两个独立语言设置:`interface_language`(界面语言)和 `ai_language`(AI 回复语言),用户需分别设置。本任务将其统一为单一 `language` 字段。 + +## 2. 目标 + +1. 合并 `interface_language` + `ai_language` → `language` +2. 后端 AI 统一使用 `language` 作为回复语言 +3. 前端移除 AI 语言 UI,设置界面只保留一个"语言"选项 +4. 文案从"界面语言"改为"语言" + +## 3. 变更内容 + +### 3.1 Schema 变更 + +**变更前**: +```json +{ + "preferences": { + "interface_language": "zh-CN", + "ai_language": "zh-CN" + } +} +``` + +**变更后**: +```json +{ + "preferences": { + "language": "zh-CN" + } +} +``` + +### 3.2 UI 变更 + +**变更前**: 两个独立选项 +``` +界面语言 简体中文 > +AI回复语言 简体中文 > +``` + +**变更后**: 一个选项 +``` +语言 简体中文 > +``` + +## 4. 文件变更清单 + +### 后端 (7 文件) + +| 文件 | 变更 | +|------|------| +| `backend/src/schemas/shared/user.py` | 字段重命名 | +| `backend/src/core/agentscope/runtime/runner.py` | 读取 `language` | +| `backend/src/core/agentscope/prompts/system_prompt.py` | 参数重命名 | +| `backend/src/core/agentscope/prompts/worker_rules.py` | 参数重命名 | +| `backend/src/core/agentscope/prompts/agent_prompt.py` | 参数重命名 | +| `backend/tests/unit/test_parse_profile_settings.py` | 测试更新 | +| `backend/tests/unit/test_agentscope_prompts.py` | 测试更新 | + +### 数据库 (SQL 直接执行) + +| 操作 | 说明 | +|------|------| +| Supabase MCP SQL | 更新现有 profiles.settings JSON 结构 | + +### 前端 (6 文件) + +| 文件 | 变更 | +|------|------| +| `apps/lib/features/settings/data/models/profile_settings.dart` | 字段重命名 | +| `apps/lib/features/settings/data/apis/profile_api.dart` | 序列化字段 | +| `apps/lib/features/settings/presentation/screens/general_settings_screen.dart` | 移除 AI 语言 UI | +| `apps/lib/l10n/app_zh.arb` | 删除旧文案,添加新文案 | +| `apps/lib/l10n/app_en.arb` | 删除旧文案,添加新文案 | +| `apps/lib/l10n/app_zh_hant.arb` | 删除旧文案,添加新文案 | + +### 协议文档 (2 文件) + +| 文件 | 变更 | +|------|------| +| `docs/protocols/profile/profile-protocol.md` | 更新字段定义 | +| `docs/protocols/divination/divination-run-protocol.md` | 更新语言规则 | + +## 5. 验收标准 + +1. 后端 API 只返回 `language` 字段 +2. 前端设置界面只有一个语言选项 +3. AI 回复语言跟随 `language` 设置 +4. 所有测试通过 diff --git a/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/IMPLEMENTATION_PLAN.md b/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..02196b4 --- /dev/null +++ b/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/IMPLEMENTATION_PLAN.md @@ -0,0 +1,334 @@ +# IMPLEMENTATION_PLAN:积分流水列表功能 + +## 概述 + +本计划按 trellis 工作流制定,遵循 `schema → repository → service → router` 后端分层和 `data → presentation` 前端分层原则。 + +## 前置条件确认 + +| 条件 | 状态 | 说明 | +|------|------|------| +| `points_ledger` 表存在 | ✅ | 结构完整,有索引支持分页 | +| Repository 写入方法存在 | ✅ | `append_ledger()` 已实现 | +| 积分中心页面存在 | ✅ | `CoinCenterScreen` 可添加入口 | +| 前端 points feature 存在 | ✅ | `features/points/` 目录已存在 | + +## 实现步骤 + +### Step 1: 后端 Schema 定义 + +**文件**:`backend/src/v1/points/schemas.py` + +**新增内容**: +```python +class LedgerItem(BaseModel): + model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True) + + id: str + direction: int # 1=收入, -1=支出 + amount: int = Field(ge=1) + balance_after: int = Field(alias="balanceAfter", ge=0) + change_type: str = Field(alias="changeType") + display_text: str = Field(alias="displayText") + created_at: str = Field(alias="createdAt") + + +class LedgerListResponse(BaseModel): + model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True) + + items: list[LedgerItem] + next_cursor: str | None = Field(alias="nextCursor", default=None) + has_more: bool = Field(alias="hasMore") +``` + +### Step 2: 后端 Repository 方法 + +**文件**:`backend/src/v1/points/repository.py` + +**新增方法**: +```python +async def list_ledger( + self, + *, + user_id: UUID, + limit: int, + cursor: datetime | None = None, +) -> tuple[list[PointsLedger], bool]: + # 按 created_at DESC 分页查询 + # cursor 为上一页最后一条的 created_at + # 多查一条判断 has_more +``` + +### Step 3: 后端 Service 方法 + +**文件**:`backend/src/v1/points/service.py` + +**新增方法**: +```python +async def get_ledger_list( + self, + *, + user_id: UUID, + limit: int = 20, + cursor: str | None = None, +) -> tuple[list[LedgerItem], str | None, bool]: + # 1. 解析 cursor 为 datetime + # 2. 调用 repository.list_ledger() + # 3. 组装 display_text(根据 change_type) + # 4. 返回 (items, next_cursor, has_more) +``` + +**display_text 映射**: +```python +CHANGE_TYPE_TEXT = { + "register": ("注册赠送", "Registration bonus"), + "purchase": ("购买积分包", "Purchase credits"), + "consume": ("AI 对话消耗", "AI chat cost"), + "adjust": ("系统调整", "System adjustment"), + "refund": ("退款", "Refund"), +} +``` + +### Step 4: 后端 Router 端点 + +**文件**:`backend/src/v1/points/router.py` + +**新增端点**: +```python +@router.get("/ledger", response_model=LedgerListResponse) +async def get_points_ledger( + service: Annotated[PointsService, Depends(get_points_service)], + current_user: Annotated[CurrentUser, Depends(get_current_user)], + limit: int = Query(default=20, ge=1, le=100), + cursor: str | None = Query(default=None), +) -> LedgerListResponse: + ... +``` + +### Step 5: 前端 Model 定义 + +**文件**:`apps/lib/features/points/data/models/ledger_item.dart` + +```dart +class LedgerItem { + const LedgerItem({ + required this.id, + required this.direction, + required this.amount, + required this.balanceAfter, + required this.changeType, + required this.displayText, + required this.createdAt, + }); + + final String id; + final int direction; // 1=收入, -1=支出 + final int amount; + final int balanceAfter; + final String changeType; + final String displayText; + final String createdAt; + + factory LedgerItem.fromJson(Map json) => ...; +} + +class LedgerListResult { + const LedgerListResult({ + required this.items, + this.nextCursor, + required this.hasMore, + }); + + final List items; + final String? nextCursor; + final bool hasMore; +} +``` + +### Step 6: 前端 API 方法 + +**文件**:`apps/lib/features/points/data/apis/points_api.dart` + +**新增方法**: +```dart +Future getLedger({ + int limit = 20, + String? cursor, +}) async { + final query = {'limit': limit}; + if (cursor != null) query['cursor'] = cursor; + final response = await _dio.get('/api/v1/points/ledger', queryParameters: query); + return LedgerListResult.fromJson(response.data); +} +``` + +### Step 7: 前端流水列表页面 + +**文件**:`apps/lib/features/points/presentation/screens/points_ledger_screen.dart` + +**功能**: +- 标题:积分流水 +- 列表项:类型图标 + 文案 + 金额(颜色区分) + 时间 +- 分页加载:滚动到底部加载更多 +- 空状态:暂无流水记录 +- Loading 状态 + +### Step 8: 重命名 AccountDeleteScreen 为 AccountDataScreen + +**文件操作**: +1. 重命名文件:`account_delete_screen.dart` → `account_data_screen.dart` +2. 重命名类:`AccountDeleteScreen` → `AccountDataScreen` +3. 更新类内状态名:`_AccountDeleteScreenState` → `_AccountDataScreenState` + +**页面结构变更**: +```dart +// 之前:只有删除账号入口 +body: ListView( + children: [ + SettingsGroupCard( + children: [ + SettingsMenuTile(icon: Icons.delete_outline_rounded, ...), + ], + ), + ], +), + +// 之后:积分流水 + 删除账号 +body: ListView( + children: [ + SettingsGroupCard( + children: [ + SettingsMenuTile( + icon: Icons.receipt_long_rounded, + title: l10n.pointsLedgerTitle, // 积分流水 + tint: colors.primary, + background: colors.surfaceContainerHighest, + onTap: _openPointsLedger, + ), + SettingsMenuTile( + icon: Icons.delete_outline_rounded, + title: l10n.settingsDeleteAccountTitle, + tint: colors.error, + background: colors.surfaceContainerHighest, + titleColor: colors.error, + showDivider: false, + onTap: _confirmDelete, + ), + ], + ), + ], +), +``` + +**新增方法**: +```dart +Future _openPointsLedger() async { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => PointsLedgerScreen( + apiClient: widget.apiClient, + userId: widget.userId, + ), + ), + ); +} +``` + +**新增 widget 参数**: +```dart +class AccountDataScreen extends StatefulWidget { + const AccountDataScreen({ + super.key, + required this.onDeleteAccount, + required this.apiClient, // 新增 + required this.userId, // 新增 + }); + ... +} +``` + +### Step 9: 更新 SettingsScreen 导入和调用 + +**文件**:`apps/lib/features/settings/presentation/screens/settings_screen.dart` + +**修改**: +1. 更新导入:`import 'account_data_screen.dart';`(替换原 `account_delete_screen.dart`) +2. 更新 `_openAccountDelete()` 方法,传递新增参数: +```dart +Future _openAccountData() async { + final deleted = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => AccountDataScreen( + onDeleteAccount: widget.onDeleteAccount, + apiClient: widget.apiClient, + userId: widget.userId, + ), + ), + ); + ... +} +``` +3. 更新菜单项标题(如需要) + +### Step 10: 添加 i18n 文案 + +**文件**: +- `apps/lib/l10n/app_zh.arb` +- `apps/lib/l10n/app_en.arb` +- `apps/lib/l10n/app_zh_hant.arb` + +**新增文案**: +```json +"pointsLedgerTitle": "积分流水", +"pointsLedgerEmpty": "暂无流水记录", +"pointsLedgerLoadingMore": "加载中..." +``` + +## 验证步骤 + +### 后端验证 + +```bash +# 启动后端 +./infra/scripts/app.sh start + +# 测试 API +curl -H "Authorization: Bearer " \ + "http://localhost:8000/api/v1/points/ledger?limit=10" +``` + +### 前端验证 + +```bash +cd apps +flutter run +# 导航到设置 -> 积分中心 -> 点击「查看流水」 +``` + +## 风险与注意事项 + +1. **RLS 策略**:`points_ledger` 表已有 RLS,确保查询只返回当前用户数据 +2. **游标分页**:使用 `created_at` 作为游标,注意处理相同时间戳的情况 +3. **性能**:索引 `ix_points_ledger_user_created_at` 已存在,查询性能有保障 +4. **空状态**:新用户可能无流水,需处理空列表情况 + +## 文件变更清单 + +### 后端新增/修改 + +| 文件 | 操作 | 说明 | +|------|------|------| +| `backend/src/v1/points/schemas.py` | 修改 | 新增 LedgerItem、LedgerListResponse | +| `backend/src/v1/points/repository.py` | 修改 | 新增 list_ledger() | +| `backend/src/v1/points/service.py` | 修改 | 新增 get_ledger_list() | +| `backend/src/v1/points/router.py` | 修改 | 新增 GET /ledger 端点 | + +### 前端新增/修改 + +| 文件 | 操作 | 说明 | +|------|------|------| +| `apps/lib/features/points/data/models/ledger_item.dart` | 新增 | 流水项模型 | +| `apps/lib/features/points/data/apis/points_api.dart` | 修改 | 新增 getLedger() | +| `apps/lib/features/points/presentation/screens/points_ledger_screen.dart` | 新增 | 流水列表页面 | +| `apps/lib/features/settings/presentation/screens/coin_center_screen.dart` | 修改 | 添加入口按钮 | +| `apps/lib/l10n/app_*.arb` | 修改 | 新增文案 | diff --git a/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/prd.md b/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/prd.md new file mode 100644 index 0000000..1fcace7 --- /dev/null +++ b/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/prd.md @@ -0,0 +1,201 @@ +# PRD:积分流水列表功能 + +## 1. 背景与目标 + +当前应用已有完整的积分账户系统: +- 积分账户表:`user_points` +- 积分业务流水:`points_ledger`(记录所有积分变动) +- 积分余额查询接口:`GET /api/v1/points/balance` +- 积分中心页面:`CoinCenterScreen` + +用户在积分中心可以看到余额和购买套餐,但无法查看积分的收支明细。本任务目标是增加积分流水列表功能,让用户了解自己的积分变动历史。 + +## 2. 当前事实 + +### 2.1 后端现状 + +**已有的 `points_ledger` 表结构**: +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | UUID | 主键 | +| `user_id` | UUID | 用户 ID | +| `direction` | smallint | 方向:1=收入,-1=支出 | +| `amount` | bigint | 变动数量(正数) | +| `balance_after` | bigint | 变动后余额 | +| `change_type` | varchar | 类型:register/consume/adjust/purchase/refund | +| `biz_type` | varchar | 业务类型:chat/payment(可空) | +| `biz_id` | UUID | 业务 ID(可空) | +| `event_id` | varchar | 幂等事件 ID | +| `operator_id` | UUID | 操作者 ID(可空) | +| `metadata` | jsonb | 扩展元数据 | +| `created_at` | timestamptz | 创建时间 | + +**已有的索引**: +- `ix_points_ledger_user_created_at`:支持按用户+时间倒序查询 +- `uq_points_ledger_user_event`:用户+事件唯一约束 + +**已有的 Repository 方法**: +- `append_ledger()`:写入流水 +- `has_ledger_event()`:检查事件是否存在 + +**缺失**: +- 无分页查询流水列表方法 +- 无 HTTP API 端点 + +### 2.2 前端现状 + +**已有的积分中心页面**: +- `apps/lib/features/settings/presentation/screens/coin_center_screen.dart` +- 显示余额和套餐卡片 + +**已有的 API 调用**: +- `apps/lib/features/points/data/apis/points_api.dart`:仅支持 `getPackages()` + +**缺失**: +- 无流水列表 API 调用 +- 无流水列表页面/组件 + +## 3. 需求范围 + +### 3.1 必须实现 + +**后端**: +- 新增 `GET /api/v1/points/ledger` 接口 +- 支持分页(游标分页,按 `created_at` 倒序) +- 返回流水列表,包含:时间、类型、金额、余额、描述 + +**前端**: +- 在积分中心页面添加「查看流水」入口 +- 新建流水列表页面,支持分页加载 +- 流水项展示:类型图标、类型文案、金额(+绿色/-红色)、时间、余额 + +### 3.2 本期不做 + +- 按类型筛选(可作为后续优化) +- 导出流水 +- 流水详情页 + +## 4. 数据契约 + +### 4.1 API 接口 + +**请求**: +``` +GET /api/v1/points/ledger?limit=20&cursor={created_at_iso} +``` + +**响应**: +```json +{ + "items": [ + { + "id": "uuid", + "direction": 1, + "amount": 100, + "balanceAfter": 500, + "changeType": "purchase", + "displayText": "购买积分包", + "createdAt": "2026-04-28T10:00:00Z" + } + ], + "nextCursor": "2026-04-27T10:00:00Z", + "hasMore": true +} +``` + +### 4.2 change_type 对应展示文案 + +| change_type | direction | 展示文案(中文) | 展示文案(英文) | +|-------------|-----------|------------------|------------------| +| register | 1 | 注册赠送 | Registration bonus | +| purchase | 1 | 购买积分包 | Purchase credits | +| consume | -1 | AI 对话消耗 | AI chat cost | +| adjust | ±1 | 系统调整 | System adjustment | +| refund | -1 | 退款 | Refund | + +## 5. 技术方案 + +### 5.1 后端实现 + +**Repository 层**(`v1/points/repository.py`): +```python +async def list_ledger( + self, + *, + user_id: UUID, + limit: int, + cursor: datetime | None = None, +) -> tuple[list[PointsLedger], bool]: + # 查询 points_ledger 表 + # 按 created_at DESC 分页 + # 返回 (items, has_more) +``` + +**Service 层**(`v1/points/service.py`): +```python +async def get_ledger_list( + self, + *, + user_id: UUID, + limit: int = 20, + cursor: str | None = None, +) -> LedgerListResult: + # 调用 repository + # 组装 display_text + # 返回响应 +``` + +**Router 层**(`v1/points/router.py`): +```python +@router.get("/ledger", response_model=LedgerListResponse) +async def get_points_ledger(...): + ... +``` + +### 5.2 前端实现 + +**目录结构**: +``` +apps/lib/features/points/ +├── data/ +│ ├── apis/ +│ │ └── points_api.dart # 新增 getLedger() +│ └── models/ +│ ├── package_info.dart # 已有 +│ └── ledger_item.dart # 新增 +└── presentation/ + └── screens/ + └── points_ledger_screen.dart # 新增 +``` + +**入口**: +- 在 `CoinCenterScreen` 的余额卡片下方添加「查看流水」按钮 +- 点击后导航到 `PointsLedgerScreen` + +## 6. 实现步骤 + +### Phase 1: 后端 API + +1. 新增 Schema:`LedgerItem`、`LedgerListResponse`(`v1/points/schemas.py`) +2. 新增 Repository 方法:`list_ledger()`(`v1/points/repository.py`) +3. 新增 Service 方法:`get_ledger_list()`(`v1/points/service.py`) +4. 新增 Router 端点:`GET /ledger`(`v1/points/router.py`) +5. 编写单元测试 + +### Phase 2: 前端 UI + +1. 新增 Model:`LedgerItem`(`features/points/data/models/ledger_item.dart`) +2. 新增 API 方法:`getLedger()`(`features/points/data/apis/points_api.dart`) +3. 新增页面:`PointsLedgerScreen`(`features/points/presentation/screens/points_ledger_screen.dart`) +4. 修改 `CoinCenterScreen`,添加入口按钮 +5. 添加 i18n 文案(`app_zh.arb`、`app_en.arb`、`app_zh_hant.arb`) + +## 7. 验收标准 + +- [ ] 后端 API 返回正确的分页数据 +- [ ] 前端能正确加载并展示流水列表 +- [ ] 流水类型显示对应文案 +- [ ] 金额按收支方向显示不同颜色 +- [ ] 分页加载正常工作 +- [ ] 无数据时显示空状态 +- [ ] 加载中显示 loading 状态 diff --git a/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/task.json b/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/task.json new file mode 100644 index 0000000..0e91c31 --- /dev/null +++ b/.trellis/tasks/archive/2026-04/04-28-feat-points-ledger/task.json @@ -0,0 +1,85 @@ +{ + "title": "feat: 积分流水列表功能", + "slug": "feat-points-ledger", + "status": "completed", + "created_at": "2026-04-28", + "developer": "opencode", + "description": "增加积分流水列表功能,入口放在「账号与数据」页面", + "prd": "prd.md", + "implementation_plan": "IMPLEMENTATION_PLAN.md", + "checklist": [ + { + "phase": "backend", + "items": [ + { + "task": "新增 LedgerItem、LedgerListResponse Schema", + "file": "backend/src/v1/points/schemas.py", + "done": true + }, + { + "task": "新增 list_ledger() Repository 方法", + "file": "backend/src/v1/points/repository.py", + "done": true + }, + { + "task": "新增 get_ledger_list() Service 方法", + "file": "backend/src/v1/points/service.py", + "done": true + }, + { + "task": "新增 GET /ledger Router 端点", + "file": "backend/src/v1/points/router.py", + "done": true + }, + { + "task": "后端 API 测试通过", + "done": true + } + ] + }, + { + "phase": "frontend", + "items": [ + { + "task": "新增 LedgerItem 模型", + "file": "apps/lib/features/points/data/models/ledger_item.dart", + "done": true + }, + { + "task": "新增 getLedger() API 方法", + "file": "apps/lib/features/points/data/apis/points_api.dart", + "done": true + }, + { + "task": "新增 PointsLedgerScreen 页面", + "file": "apps/lib/features/points/presentation/screens/points_ledger_screen.dart", + "done": true + }, + { + "task": "重命名 AccountDeleteScreen → AccountDataScreen,添加积分流水入口", + "file": "apps/lib/features/settings/presentation/screens/account_data_screen.dart", + "done": true + }, + { + "task": "更新 SettingsScreen 导入和调用", + "file": "apps/lib/features/settings/presentation/screens/settings_screen.dart", + "done": true + }, + { + "task": "添加 i18n 文案", + "file": "apps/lib/l10n/app_*.arb", + "done": true + }, + { + "task": "运行 flutter gen-l10n 生成代码", + "done": true + }, + { + "task": "前端功能测试通过", + "done": true + } + ] + } + ], + "completedAt": "2026-04-28" +} \ No newline at end of file