8.8 KiB
8.8 KiB
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
新增内容:
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
新增方法:
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
新增方法:
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 映射:
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
新增端点:
@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
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<String, dynamic> json) => ...;
}
class LedgerListResult {
const LedgerListResult({
required this.items,
this.nextCursor,
required this.hasMore,
});
final List<LedgerItem> items;
final String? nextCursor;
final bool hasMore;
}
Step 6: 前端 API 方法
文件:apps/lib/features/points/data/apis/points_api.dart
新增方法:
Future<LedgerListResult> 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
文件操作:
- 重命名文件:
account_delete_screen.dart→account_data_screen.dart - 重命名类:
AccountDeleteScreen→AccountDataScreen - 更新类内状态名:
_AccountDeleteScreenState→_AccountDataScreenState
页面结构变更:
// 之前:只有删除账号入口
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,
),
],
),
],
),
新增方法:
Future<void> _openPointsLedger() async {
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => PointsLedgerScreen(
apiClient: widget.apiClient,
userId: widget.userId,
),
),
);
}
新增 widget 参数:
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
修改:
- 更新导入:
import 'account_data_screen.dart';(替换原account_delete_screen.dart) - 更新
_openAccountDelete()方法,传递新增参数:
Future<void> _openAccountData() async {
final deleted = await Navigator.of(context).push<bool>(
MaterialPageRoute<bool>(
builder: (_) => AccountDataScreen(
onDeleteAccount: widget.onDeleteAccount,
apiClient: widget.apiClient,
userId: widget.userId,
),
),
);
...
}
- 更新菜单项标题(如需要)
Step 10: 添加 i18n 文案
文件:
apps/lib/l10n/app_zh.arbapps/lib/l10n/app_en.arbapps/lib/l10n/app_zh_hant.arb
新增文案:
"pointsLedgerTitle": "积分流水",
"pointsLedgerEmpty": "暂无流水记录",
"pointsLedgerLoadingMore": "加载中..."
验证步骤
后端验证
# 启动后端
./infra/scripts/app.sh start
# 测试 API
curl -H "Authorization: Bearer <token>" \
"http://localhost:8000/api/v1/points/ledger?limit=10"
前端验证
cd apps
flutter run
# 导航到设置 -> 积分中心 -> 点击「查看流水」
风险与注意事项
- RLS 策略:
points_ledger表已有 RLS,确保查询只返回当前用户数据 - 游标分页:使用
created_at作为游标,注意处理相同时间戳的情况 - 性能:索引
ix_points_ledger_user_created_at已存在,查询性能有保障 - 空状态:新用户可能无流水,需处理空列表情况
文件变更清单
后端新增/修改
| 文件 | 操作 | 说明 |
|---|---|---|
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 |
修改 | 新增文案 |