202 lines
5.6 KiB
Markdown
202 lines
5.6 KiB
Markdown
# 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 状态
|