# 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` | 修改 | 新增文案 |