新增:历史记录对接文档
This commit is contained in:
parent
8c15058b96
commit
fe0e81a5f2
92
docs/credit_record_requirements_and_implementation.md
Normal file
92
docs/credit_record_requirements_and_implementation.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Credit Record 需求与实现方案(框架对齐)
|
||||||
|
|
||||||
|
本文档独立说明 `Credit Record`(积分流水)模块,避免与生成历史记录(`My History`)说明混排。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块目标与范围
|
||||||
|
|
||||||
|
`Credit Record` 用于展示用户积分变动流水,支持:
|
||||||
|
|
||||||
|
- 首屏加载
|
||||||
|
- 下拉刷新
|
||||||
|
- 滚动分页加载(Load More)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 展示数据规则
|
||||||
|
|
||||||
|
每条流水展示:
|
||||||
|
|
||||||
|
- **积分变动值**(由 `type + credits` 决定)
|
||||||
|
- `type == 1`:展示 `+N`
|
||||||
|
- `type == 2`:展示 `-N`
|
||||||
|
- 兜底按 `credits` 正负展示
|
||||||
|
- **日期**:`createTime` 格式化为 `yyyy/MM/dd`
|
||||||
|
|
||||||
|
空态文案:`No records.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 接口对接
|
||||||
|
|
||||||
|
积分流水接口:
|
||||||
|
|
||||||
|
- `UserApi.getCreditsPage`
|
||||||
|
- 对应后端:`GET /v1/user/credits-page`
|
||||||
|
- 参数:
|
||||||
|
- `page`: 页码(从 1 开始)
|
||||||
|
- `size`: 每页条数(当前 30)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 分页加载实现方案
|
||||||
|
|
||||||
|
### 4.1 状态字段
|
||||||
|
|
||||||
|
建议维护以下分页状态(字段命名由各客户端自行定义):
|
||||||
|
|
||||||
|
- `pageSize`(建议 30)
|
||||||
|
- `loading`
|
||||||
|
- `loadingMore`
|
||||||
|
- `hasMore`
|
||||||
|
- `records`
|
||||||
|
- `lastLoadedPage`
|
||||||
|
- `listGeneration`(刷新版本号,用于隔离过期请求)
|
||||||
|
|
||||||
|
### 4.2 首屏与刷新
|
||||||
|
|
||||||
|
1. 进入页面或下拉刷新时请求 `page=1`。
|
||||||
|
2. 清空旧数据并重置分页状态。
|
||||||
|
3. 成功后覆盖列表并更新 `lastLoadedPage` 与 `hasMore`。
|
||||||
|
|
||||||
|
### 4.3 滚动加载更多
|
||||||
|
|
||||||
|
1. 监听滚动位置,接近底部阈值(约 240px)触发。
|
||||||
|
2. 若 `loading/loadingMore/hasMore` 条件不满足则不发起请求。
|
||||||
|
3. 请求 `nextPage = lastLoadedPage + 1`。
|
||||||
|
4. 成功后 `addAll` 追加并更新分页状态。
|
||||||
|
|
||||||
|
### 4.4 hasMore 判定优先级
|
||||||
|
|
||||||
|
1. 优先使用后端 `pages/current`
|
||||||
|
2. 其次使用 `total`
|
||||||
|
3. 最后兜底 `incoming.length >= pageSize`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 异常与边界处理
|
||||||
|
|
||||||
|
- 登录失败:显示错误态 `Sign in failed`
|
||||||
|
- 接口失败:保留当前列表并停止 `loadingMore`
|
||||||
|
- 空数据:显示 `No records.`
|
||||||
|
- 刷新期间旧请求返回:通过 `listGeneration` 丢弃
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 验收清单
|
||||||
|
|
||||||
|
1. 首屏、下拉刷新、滚动分页三种路径都可稳定工作。
|
||||||
|
2. 快速滚动不出现重复数据或错序。
|
||||||
|
3. 接口异常时不导致列表清空闪退。
|
||||||
|
4. 积分符号展示与 `type` 语义一致。
|
||||||
142
docs/history_page_requirements_and_implementation.md
Normal file
142
docs/history_page_requirements_and_implementation.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# 生成历史记录需求与实现方案(框架对齐)
|
||||||
|
|
||||||
|
本文档仅面向 `My History`(生成历史记录)模块的产品需求、数据展示规则、接口对接与交互实现说明,供客户端与 `client_proxy_framework` 对齐开发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 页面目标与范围
|
||||||
|
|
||||||
|
`My History` 用于展示用户已创建任务的结果与状态,支持查看、下载、删除、继续追踪进度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. My History 需求定义
|
||||||
|
|
||||||
|
### 2.1 展示数据字段(卡片)
|
||||||
|
|
||||||
|
每个历史卡片展示以下信息:
|
||||||
|
|
||||||
|
- **封面图**(见 2.2 封面取值规则)
|
||||||
|
- **创建日期**:由 `MyTaskItem.createTime` 格式化为 `MMM d, yyyy`
|
||||||
|
- **24 小时剩余时间**:`createTime + 24h - now`
|
||||||
|
- 剩余时间大于 0:展示 `xh xxm left`
|
||||||
|
- 小于等于 0:展示 `Expired`
|
||||||
|
- **操作区**:
|
||||||
|
- 支持按客户端配置决定是否在卡片上展示 `Download` 入口(非必选)
|
||||||
|
- 不展示下载入口时,操作区可仅展示状态文案(由任务状态映射)
|
||||||
|
- 若客户端采用“详情页下载”方案,则卡片点击进入详情页后再提供下载能力
|
||||||
|
- **删除按钮**:可触发删除确认弹窗
|
||||||
|
|
||||||
|
顶部需展示“有效期提醒”提示(文案可换皮配置,不要求固定英文):
|
||||||
|
|
||||||
|
- 语义必须包含两点:
|
||||||
|
1. 任务内容仅保留 24 小时(或 1 天);
|
||||||
|
2. 过期前需要下载保存。
|
||||||
|
- 各换皮应用可按品牌语气自定义标题与正文,但不得改变上述核心含义。
|
||||||
|
- 示例语义(非固定文案):
|
||||||
|
- 标题:`24-hour expiry` / `Available for 24 hours`
|
||||||
|
- 正文:`Each item is kept for 24 hours after creation. Download before it expires.`
|
||||||
|
|
||||||
|
### 2.2 封面取值规则(重点)
|
||||||
|
|
||||||
|
封面来源按以下优先级:
|
||||||
|
|
||||||
|
1. **优先使用 `resultUrl`(网络封面)**
|
||||||
|
- `resultUrl` 是 `http/https` 且为图片地址:直接展示网络图。
|
||||||
|
2. **若 `resultUrl` 判定为视频地址**(如 `.mp4/.m3u8/.webm/.mov`):
|
||||||
|
- 优先使用本地封面 `localCoverPath`(由 `ImageTaskHistory.localCoverPathsForMyTaskItems(tasks)` 提供)。
|
||||||
|
- 若本地封面不存在,展示视频占位图(摄像机 icon)。
|
||||||
|
3. **若 `resultUrl` 不可用**:
|
||||||
|
- 回退使用 `localCoverPath`。
|
||||||
|
4. **仍无可用封面**:
|
||||||
|
- 展示默认背景色占位块。
|
||||||
|
|
||||||
|
### 2.3 接口对接
|
||||||
|
|
||||||
|
历史列表接口:
|
||||||
|
|
||||||
|
- `ImageApi.getMyTasks`
|
||||||
|
- 对应后端:`GET /v1/image/my-tasks`
|
||||||
|
- 当前请求参数:
|
||||||
|
- `app`: `currentBackendAppType()`
|
||||||
|
- `page`: `'1'`
|
||||||
|
- `pageSize`: `'30'`
|
||||||
|
|
||||||
|
删除任务接口:
|
||||||
|
|
||||||
|
- `ImageApi.deleteTask`
|
||||||
|
- 入参:`taskId`(int)
|
||||||
|
- 成功后前端需从列表移除对应卡片并清理本地封面映射缓存。
|
||||||
|
|
||||||
|
任务进度接口(点击生成中任务后轮询):
|
||||||
|
|
||||||
|
- `ImageApi.getProgress`
|
||||||
|
- 对应后端:`GET /v1/image/progress`
|
||||||
|
|
||||||
|
### 2.4 点击行为与跳转
|
||||||
|
|
||||||
|
卡片点击按任务状态分流:
|
||||||
|
|
||||||
|
1. **有远端结果地址**
|
||||||
|
跳转任务结果详情页(携带 `taskId`、`resultUrl`)。
|
||||||
|
2. **任务生成中**
|
||||||
|
跳转任务进度页,建议每 5 秒轮询一次进度接口,成功后自动进入结果详情页。
|
||||||
|
3. **状态显示完成但媒体地址未就绪**
|
||||||
|
提示:`Media is not ready yet. Pull to refresh.`
|
||||||
|
4. **其余不可进入状态**
|
||||||
|
根据状态映射给出阻塞提示文案。
|
||||||
|
|
||||||
|
辅助交互(支持按客户端策略配置):
|
||||||
|
|
||||||
|
- **Download 点击(若卡片启用该入口)**:下载 `resultUrl` 到系统相册(图片/视频分流保存)。
|
||||||
|
- **详情页下载(若卡片不提供下载入口)**:进入结果详情页后再执行下载。
|
||||||
|
- **Delete 点击**:先弹窗二次确认,再调用删除接口,成功后移除条目并提示 `Deleted`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. My History 分页加载方案(建议落地)
|
||||||
|
|
||||||
|
建议客户端按统一分页模式实现 `My History`,支持首屏加载、下拉刷新与滚动加载更多。
|
||||||
|
|
||||||
|
### 3.1 增加分页状态
|
||||||
|
|
||||||
|
建议维护以下分页状态(命名由各客户端自行定义):
|
||||||
|
|
||||||
|
- `pageSize`(建议 30)
|
||||||
|
- `lastLoadedPage`
|
||||||
|
- `hasMore`
|
||||||
|
- `loadingMore`
|
||||||
|
- `listGeneration`(用于刷新隔离)
|
||||||
|
|
||||||
|
### 3.2 请求策略
|
||||||
|
|
||||||
|
1. **首屏/下拉刷新**:请求第 1 页,覆盖列表。
|
||||||
|
2. **滚动到底触发**:请求 `nextPage = lastLoadedPage + 1`,并追加数据。
|
||||||
|
3. **hasMore 判定**:优先使用后端分页字段(若有),否则按返回条数兜底。
|
||||||
|
4. **封面映射增量更新**:对新增任务批次补齐本地封面映射并合并到当前缓存。
|
||||||
|
|
||||||
|
### 3.3 去重与一致性
|
||||||
|
|
||||||
|
- 追加分页时按 `taskId` 去重,避免刷新/重试导致重复卡片。
|
||||||
|
- 删除任务后同步从列表数据与封面缓存中移除。
|
||||||
|
- 当用户从进度页返回后,可触发轻量刷新(仅首屏页)保障状态最新。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 异常与边界处理
|
||||||
|
|
||||||
|
- 登录未完成:展示 loading;登录失败展示错误态。
|
||||||
|
- 接口失败:展示错误文案 + Retry。
|
||||||
|
- 空数据:`My History` 显示 `No tasks yet.`
|
||||||
|
- 非法 `taskId`:删除时拦截并提示 `Invalid task id`。
|
||||||
|
- 下载失败:展示 `Save failed` 或系统权限错误。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 验收清单(建议)
|
||||||
|
|
||||||
|
1. `My History` 封面优先级符合 2.2(图片、视频、本地回退、占位)。
|
||||||
|
2. 卡片点击分流准确(结果页/进度页/提示)。
|
||||||
|
3. 删除成功后 UI 与本地封面缓存同步移除。
|
||||||
|
4. `My History` 分页补齐后,快滑场景无重复、无错序,且 `hasMore` 判定稳定。
|
||||||
|
|
||||||
@ -1,298 +1,198 @@
|
|||||||
# 支付流程文档
|
# 支付流程(多客户端通用)
|
||||||
|
|
||||||
## 1. 概述
|
## 1. 概述
|
||||||
|
|
||||||
本文档描述 Android 项目(Flutter)的完整支付流程,包括商品获取、支付方式选择、订单创建、Google Play 内购以及补单机制。
|
本文档描述换皮应用可复用的支付全流程,覆盖:商品获取、支付方式选择、订单创建、平台内购回调、订单核销、补单与账户刷新。
|
||||||
|
文档以“能力与流程”表达,不绑定具体客户端项目、目录路径或某一语言实现。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 支付流程总览
|
## 2. 支付流程总览
|
||||||
|
|
||||||
```
|
```text
|
||||||
用户点击 Buy
|
用户点击充值 / Buy
|
||||||
│
|
│
|
||||||
├─ enableThirdPartyPayment === true 且已登录
|
├─ 已登录 且 开启第三方支付
|
||||||
│ │
|
│ │
|
||||||
│ ├─ getPaymentMethods(activityId) 获取支付方式
|
│ ├─ 拉取支付方式(基于商品/活动)
|
||||||
│ ├─ 弹窗选择支付方式
|
│ ├─ 用户选择支付方式
|
||||||
│ ├─ createPayment 创建订单
|
│ ├─ 创建订单(服务端生成业务订单)
|
||||||
│ │
|
│ │
|
||||||
│ ├─ 若选中的是 Google Pay
|
│ ├─ 若为平台内购(如 Google Play)
|
||||||
│ │ ├─ 调起 Google Play 内购
|
│ │ ├─ 调起商店购买
|
||||||
│ │ ├─ 拿到 purchaseData + signature
|
│ │ ├─ 获取 purchaseData + signature + storeOrderId
|
||||||
│ │ └─ googlepay 回调验证
|
│ │ └─ 回调服务端校验与入账
|
||||||
│ │
|
│ │
|
||||||
│ └─ 否则(其他支付方式)
|
│ └─ 若为外部支付
|
||||||
│ └─ 打开 payUrl 在外部浏览器完成支付
|
│ └─ 打开 payUrl 在 Web/外部浏览器完成支付
|
||||||
│
|
│
|
||||||
└─ enableThirdPartyPayment !== true 或未登录
|
└─ 未登录 或 未开启第三方支付
|
||||||
└─ 仅 Android:直接调起 Google Play 内购
|
└─ 可走平台内购直购分支(由业务策略决定)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 支付分支依据
|
## 3. 支付分支依据(通用)
|
||||||
|
|
||||||
| 条件 | 说明 |
|
| 条件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `UserState.enableThirdPartyPayment` | 登录后由 AuthService 从 `/v1/user/common_info` 响应写入 |
|
| 是否登录 | 通常需要用户身份用于订单归属与回调入账 |
|
||||||
| `UserState.userId` | 用户登录后存储的用户 ID |
|
| 是否开启第三方支付 | 由服务端用户配置或渠道策略控制 |
|
||||||
| **第三方支付** | `enableThirdPartyPayment == true` 且 `userId` 非空 |
|
| 当前平台能力 | Android / iOS 对内购与外部支付支持差异 |
|
||||||
| **直接谷歌支付** | 其他情况(未开第三方支付或未登录)|
|
| 商品配置 | 某些商品只允许特定支付方式 |
|
||||||
|
|
||||||
|
建议将分支判断集中在统一策略层,避免同一逻辑散落在多个页面。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 商品展示与获取
|
## 4. 商品展示与获取
|
||||||
|
|
||||||
### 4.1 接口
|
### 4.1 获取时机
|
||||||
|
|
||||||
```dart
|
- 进入充值页时拉取一次;
|
||||||
// Android(app 默认 backendAppTypeAndroid,可传 client)
|
- 下拉刷新或切换国家/渠道后重新拉取;
|
||||||
final res = await PaymentApi.getGooglePayActivities(
|
- 支付成功后建议刷新,确保档位与余额展示一致。
|
||||||
country: '国家', // 可选
|
|
||||||
);
|
|
||||||
|
|
||||||
// iOS(app 默认 backendAppTypeIOS)
|
### 4.2 商品字段(通用语义)
|
||||||
final res = await PaymentApi.getApplePayActivities(
|
|
||||||
country: '国家', // 可选
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 返回实体
|
| 字段语义 | 说明 |
|
||||||
|
|------|------|
|
||||||
```dart
|
| `productId` | 商店商品 ID(内购必填) |
|
||||||
class PaymentProductsResponse {
|
| `activityId` | 服务端活动/档位 ID(建单常用) |
|
||||||
List<PaymentProductItem>? productList;
|
| `actualAmount` | 实付金额 |
|
||||||
}
|
| `originAmount` | 原价(可用于划线价) |
|
||||||
|
| `bonus` / `bonusCredits` | 赠送积分 |
|
||||||
class PaymentProductItem {
|
| `credits` | 到账积分(如与赠送分开展示) |
|
||||||
String? productId; // 商品 ID (对应 helm)
|
| `title` | 商品文案(可能由服务端直接下发) |
|
||||||
String? activityId; // 活动 ID (对应 warrior)
|
|
||||||
String? actualAmount; // 实际金额 (对应 guardian)
|
|
||||||
String? originAmount; // 原价 (对应 curriculum)
|
|
||||||
int? bonus; // 赠送积分 (对应 forge)
|
|
||||||
String? title; // 标题 (对应 glossary)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 第三方支付流程
|
## 5. 第三方支付流程(通用)
|
||||||
|
|
||||||
### 5.1 获取支付方式
|
### 5.1 拉取支付方式
|
||||||
|
|
||||||
```dart
|
以商品或活动为上下文请求支付方式列表,常见字段包括:
|
||||||
final res = await PaymentApi.getPaymentMethods(
|
|
||||||
activityId: 12345, // int
|
|
||||||
country: '国家', // 可选
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.isSuccess) {
|
- `paymentMethod`:主支付方式(例如平台内购、钱包、卡支付等)
|
||||||
final methods = res.data!.paymentMethods;
|
- `subPaymentMethod`:子方式(可选)
|
||||||
// methods 包含:
|
- `name` / `icon`:展示信息
|
||||||
// - paymentMethod: 支付方式 (如 GOOGLEPAY)
|
- `recommend`:是否推荐
|
||||||
// - subPaymentMethod: 子支付方式
|
|
||||||
// - name: 显示名称
|
|
||||||
// - icon: 图标 URL
|
|
||||||
// - recommend: 是否推荐
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 创建订单
|
### 5.2 创建订单
|
||||||
|
|
||||||
```dart
|
创建订单请求通常包含:
|
||||||
final res = await PaymentApi.createPayment(
|
|
||||||
app: 'HAndroid', // HIOS / HAndroid
|
|
||||||
userId: '用户ID',
|
|
||||||
activityId: '活动ID',
|
|
||||||
paymentMethod: '支付方式',
|
|
||||||
paymentType: '支付子类型', // 可选
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.isSuccess) {
|
- 应用标识(`app`)
|
||||||
final order = res.data!;
|
- 用户标识(`userId`)
|
||||||
final orderId = order.orderId; // 订单 ID
|
- 商品或活动标识(`activityId` / `productId`)
|
||||||
final payUrl = order.payUrl; // 支付链接
|
- 支付方式(`paymentMethod`、可选 `paymentType`)
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 支付分支
|
响应通常返回:
|
||||||
|
|
||||||
- **Google Pay**: 调起内购 → 获取 purchaseData/signature → 调用 `googlepay` 回调
|
- `orderId`(业务订单号)
|
||||||
- **其他方式**: 打开 `payUrl` 在外部浏览器
|
- `payUrl`(外部支付链接;仅外部支付场景)
|
||||||
|
- `status`(订单状态)
|
||||||
|
- 可选 `federation`(用于与商店订单映射)
|
||||||
|
|
||||||
|
### 5.3 分支执行
|
||||||
|
|
||||||
|
- **平台内购**:拉起商店购买 -> 取回凭据 -> 回调服务端核验 -> 成功后完成/消费交易。
|
||||||
|
- **外部支付**:打开 `payUrl` 完成支付 -> 回前台后查询订单状态或拉取账户余额。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 直接谷歌支付流程
|
## 6. 平台内购标准链路(Android/iOS 可类比)
|
||||||
|
|
||||||
仅 Android,且不经过 `getPaymentMethods` 和 `createPayment`(三方支付关闭时):
|
1. 用 `productId` 调起商店支付;
|
||||||
|
2. 拿到商店返回的交易凭据(如 `purchaseData`、`signature`、`storeOrderId`);
|
||||||
|
3. 调服务端回调接口完成验签与入账;
|
||||||
|
4. 服务端成功后,客户端执行“完成交易/消费交易”(防止重复拥有问题);
|
||||||
|
5. 刷新账户余额与订单状态。
|
||||||
|
|
||||||
```dart
|
> 关键原则:**先服务端验单成功,再本地完成/消费交易**,避免凭据丢失导致资产不一致。
|
||||||
// 1. 创建订单(直接走谷歌支付)
|
|
||||||
final createRes = await PaymentApi.createPayment(
|
|
||||||
app: 'HAndroid',
|
|
||||||
userId: '用户ID',
|
|
||||||
activityId: '活动ID',
|
|
||||||
paymentMethod: 'GooglePay',
|
|
||||||
paymentType: 'GooglePay',
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. 调起 Google Play 内购
|
|
||||||
final purchaseResult = await GooglePlayPurchaseService.launchPurchaseAndReturnData(
|
|
||||||
productId: '商品ID',
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. 回调验证
|
|
||||||
if (purchaseResult != null) {
|
|
||||||
final res = await PaymentApi.googlepay(
|
|
||||||
signature: purchaseResult.payload.signature,
|
|
||||||
purchaseData: purchaseResult.payload.purchaseData,
|
|
||||||
orderId: orderId ?? purchaseResult.orderId,
|
|
||||||
userId: userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Google Play 内购统一入口
|
## 7. 补单机制(订单恢复)
|
||||||
|
|
||||||
### 7.1 调起内购
|
### 7.1 触发时机
|
||||||
|
|
||||||
```dart
|
- 应用启动后;
|
||||||
final result = await GooglePlayPurchaseService.launchPurchaseAndReturnData(
|
- 进入充值页时;
|
||||||
productId: '商品ID',
|
- 支付异常返回时(例如网络中断、回调超时)。
|
||||||
);
|
|
||||||
|
|
||||||
if (result != null) {
|
### 7.2 恢复流程
|
||||||
// result.orderId: Google 订单号
|
|
||||||
// result.payload.purchaseData: 用于 merchant
|
|
||||||
// result.payload.signature: 用于 signature
|
|
||||||
// result.purchaseDetails: PurchaseDetails 对象
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 回调验证
|
1. 拉取未完成/未核销的商店订单(历史订单 + 监听流中的待处理订单);
|
||||||
|
2. 对每笔订单尝试找到业务订单映射(如 `federation` / `orderId`);
|
||||||
```dart
|
3. 若可映射:再次走服务端回调核验,成功后完成/消费交易;
|
||||||
final res = await PaymentApi.googlepay(
|
4. 若不可映射:按产品策略处理(仅完成/消费以解除占用,或进入人工排查队列);
|
||||||
signature: result.payload.signature,
|
5. 恢复结束后刷新账户与充值记录。
|
||||||
purchaseData: result.payload.purchaseData,
|
|
||||||
orderId: orderId,
|
|
||||||
userId: userId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.isSuccess) {
|
|
||||||
// 核销订单
|
|
||||||
await GooglePlayPurchaseService.completeAndConsumePurchase(
|
|
||||||
result.purchaseDetails,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 响应实体
|
|
||||||
|
|
||||||
```dart
|
|
||||||
class GooglePayCallbackResponse {
|
|
||||||
String? orderId;
|
|
||||||
String? status; // SUCCESS / FAILED
|
|
||||||
bool? creditsAdded; // 是否已加积分
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 补单机制
|
## 8. API 能力清单(通用命名)
|
||||||
|
|
||||||
### 8.1 触发时机
|
| 能力 | 典型方法 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
- 进入充值页时调用 `GooglePlayPurchaseService.runOrderRecovery()`
|
| 获取商品列表 | `GET` | Android/iOS 可分端点 |
|
||||||
|
| 获取支付方式 | `POST`/`GET` | 入参通常含 `activityId` |
|
||||||
### 8.2 补单流程
|
| 创建订单 | `POST` | 返回 `orderId`、`payUrl` 等 |
|
||||||
|
| 平台内购回调 | `POST` | 提交 `purchaseData`、`signature` 等 |
|
||||||
1. 获取未核销订单: `getUnacknowledgedPurchases()`
|
| 订单列表 | `GET` | 历史支付记录 |
|
||||||
- 合并 `queryPastPurchases` 和 `purchaseStream` 的待处理订单
|
| 订单详情 | `GET` | 查询单笔订单状态 |
|
||||||
|
|
||||||
2. 对每笔订单:
|
|
||||||
- 查询本地存储的 `federation` 映射
|
|
||||||
- 若存在 federation: 调用 `googlepay` 回调 → 成功后 consume
|
|
||||||
- 若无 federation: 仅执行 consume 解除「已拥有此内容」
|
|
||||||
|
|
||||||
3. 补单成功后刷新账户
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. API 汇总
|
## 9. 核心字段对照(通用)
|
||||||
|
|
||||||
| 接口 | 方法 | 返回实体 |
|
| 业务含义 | 常见请求字段 | 常见响应字段 |
|
||||||
|------|------|----------|
|
|------|------|------|
|
||||||
| `getGooglePayActivities` | GET | `PaymentProductsResponse` |
|
| 应用标识 | `app` | - |
|
||||||
| `getApplePayActivities` | GET | `PaymentProductsResponse` |
|
| 用户标识 | `userId` | - |
|
||||||
| `getPaymentMethods` | POST | `PaymentMethodsResponse` |
|
| 活动/档位 | `activityId` | - |
|
||||||
| `createPayment` | POST | `CreatePaymentResponse` |
|
| 支付方式 | `paymentMethod` | - |
|
||||||
| `getPaymentDetailList` | GET | `PaymentOrderListResponse` |
|
| 子支付方式 | `paymentType` | - |
|
||||||
| `getOrderDetail` | GET | `OrderDetailResponse` |
|
| 订单标识 | `orderId` / `id` | `orderId` |
|
||||||
| `googlepay` | POST | `GooglePayCallbackResponse` |
|
| 购买签名 | `signature` | - |
|
||||||
|
| 购买数据 | `purchaseData` | - |
|
||||||
|
| 商品 ID | - | `productId` |
|
||||||
|
| 实付金额 | - | `actualAmount` |
|
||||||
|
| 原价 | - | `originAmount` |
|
||||||
|
| 赠送积分 | - | `bonus` / `bonusCredits` |
|
||||||
|
| 支付链接 | - | `payUrl` |
|
||||||
|
| 订单状态 | - | `status` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 字段映射说明
|
## 10. 常见问题
|
||||||
|
|
||||||
框架自动完成字段映射,调用层使用原始字段名。
|
### 10.1 商品未找到
|
||||||
|
|
||||||
### 请求 → 响应 字段对照
|
- **现象**:发起内购时提示商品不存在;
|
||||||
|
- **常见原因**:客户端 `productId` 与商店后台配置不一致,或商品未上架到当前测试轨道;
|
||||||
|
- **排查建议**:核对包名/应用 ID、商品 ID、地区、测试账号与轨道配置。
|
||||||
|
|
||||||
| 业务含义 | 请求字段 | 响应字段 |
|
### 10.2 回调成功但积分未更新
|
||||||
|----------|----------|----------|
|
|
||||||
| 应用 ID | app | - |
|
- **现象**:支付侧显示成功,账户余额未变;
|
||||||
| 用户 ID | userId | - |
|
- **常见原因**:回调后未刷新账户、订单状态未落库、幂等冲突;
|
||||||
| 活动 ID | activityId | - |
|
- **排查建议**:检查回调响应、订单状态接口与账户刷新链路。
|
||||||
| 支付方式 | paymentMethod | - |
|
|
||||||
| 支付子类型 | paymentType | - |
|
### 10.3 重复购买被拦截(已拥有此内容)
|
||||||
| 订单 / 支付 ID(详情 query) | `id`(Dart 仍用参数名 orderId) | orderId |
|
|
||||||
| 购买签名 | signature | - |
|
- **现象**:商店提示已拥有,无法再次购买;
|
||||||
| 购买数据 | purchaseData | - |
|
- **常见原因**:上一笔交易未完成/未消费;
|
||||||
| 商品 ID | - | productId |
|
- **排查建议**:执行补单恢复流程并确保完成/消费逻辑成功。
|
||||||
| 实际金额 | - | actualAmount |
|
|
||||||
| 原价 | - | originAmount |
|
|
||||||
| 赠送积分 | - | bonus |
|
|
||||||
| 支付链接 | - | payUrl |
|
|
||||||
| 订单状态 | - | status |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. 代码文件位置
|
## 11. 实施建议(多客户端复用)
|
||||||
|
|
||||||
| 功能 | 文件路径 |
|
- 以“流程能力层”封装支付,不把分支逻辑写死在页面;
|
||||||
|------|----------|
|
- 页面只关心:档位展示、用户选择、状态反馈;
|
||||||
| 充值页面 | `lib/features/recharge/recharge_screen.dart` |
|
- 建单、回调、补单、轮询统一由支付服务层负责;
|
||||||
| 支付 API | `lib/core/api/services/payment_api.dart` |
|
- 统一错误码与文案映射,避免各端提示不一致;
|
||||||
| Google Play 内购服务 | `lib/features/recharge/google_play_purchase_service.dart` |
|
- 对关键节点埋点:拉起支付、建单成功、回调成功、补单成功、入账成功;
|
||||||
| 支付方式模型 | `lib/features/recharge/models/payment_method_item.dart` |
|
- 保持接口字段可映射,支持不同换皮应用的字段命名差异。
|
||||||
| 商品模型 | `lib/features/recharge/models/activity_item.dart` |
|
|
||||||
| 购买结果模型 | `lib/features/recharge/models/google_pay_purchase_result.dart` |
|
|
||||||
| WebView 支付页 | `lib/features/recharge/payment_webview_screen.dart` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. 常见问题
|
|
||||||
|
|
||||||
### 12.1 商品未找到
|
|
||||||
|
|
||||||
- 原因: 客户端 `productId` 与 Google Play 后台「产品 ID」不一致
|
|
||||||
- 排查: 检查 Play 后台产品 ID 配置
|
|
||||||
|
|
||||||
### 12.2 补单
|
|
||||||
|
|
||||||
- 未确认订单可能不会出现在 `queryPastPurchases` 中
|
|
||||||
- 应用启动时订阅 `purchaseStream` 接收重新下发
|
|
||||||
- 补单会合并两者的待处理订单
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. 注意事项
|
|
||||||
|
|
||||||
- 所有 Google Play 内购统一使用 `launchPurchaseAndReturnData()` 方法
|
|
||||||
- 回调验证成功后必须调用 `completePurchase` + `consumePurchase`
|
|
||||||
- 支付 URL 打开方式取决于 `createPayment` 返回的 `payUrl` 字段
|
|
||||||
- 订单状态轮询: 间隔 1/3/7/15/31/63 秒
|
|
||||||
|
|||||||
@ -512,6 +512,31 @@ class CreateTaskResponse extends Entity {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 删除任务响应(`POST /v1/image/delete-task` 的 `data`;见 FunyMee 文档 `hint` / `type`)。
|
||||||
|
class DeleteTaskResponse extends Entity {
|
||||||
|
DeleteTaskResponse({
|
||||||
|
this.hint,
|
||||||
|
this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? hint;
|
||||||
|
final String? type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
factory DeleteTaskResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return DeleteTaskResponse(
|
||||||
|
hint: json['hint'] as String?,
|
||||||
|
type: json['type'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'hint': hint,
|
||||||
|
'type': type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// 我的任务项
|
/// 我的任务项
|
||||||
///
|
///
|
||||||
/// 与 FunyMee 文档一致:`state`(线网 `bitrate`)1–6 为权威任务态;`status` 为兼容字段。
|
/// 与 FunyMee 文档一致:`state`(线网 `bitrate`)1–6 为权威任务态;`status` 为兼容字段。
|
||||||
|
|||||||
@ -43,14 +43,15 @@ class PaymentProductItem extends Entity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `saturation`→`bonusCredits`、`remix`→`addCredits`、`contrast`→`bonus`(赠送积分数),或 `credits`−`baseCredits`。
|
/// `saturation`→`bonusCredits`、`remix`→`addCredits`,或仅有总额时用 `credits`−`baseCredits` 推断**额外**赠送。
|
||||||
|
///
|
||||||
|
/// 不可再回退到 [json] 的 `bonus`(线网 `contrast`):该字段已由 [PaymentProductItem.bonus] 单独解析,
|
||||||
|
/// 否则 UI 会把 `bonus + bonusCredits` 加成两倍。
|
||||||
static int? _parseBonusCredits(Map<String, dynamic> json) {
|
static int? _parseBonusCredits(Map<String, dynamic> json) {
|
||||||
final direct = _intField(json, 'bonusCredits');
|
final direct = _intField(json, 'bonusCredits');
|
||||||
if (direct != null && direct > 0) return direct;
|
if (direct != null && direct > 0) return direct;
|
||||||
final add = _intField(json, 'addCredits');
|
final add = _intField(json, 'addCredits');
|
||||||
if (add != null && add > 0) return add;
|
if (add != null && add > 0) return add;
|
||||||
final contrastGift = _intField(json, 'bonus');
|
|
||||||
if (contrastGift != null && contrastGift > 0) return contrastGift;
|
|
||||||
final total = _intField(json, 'credits') ?? _intField(json, 'padding');
|
final total = _intField(json, 'credits') ?? _intField(json, 'padding');
|
||||||
final base = _intField(json, 'baseCredits');
|
final base = _intField(json, 'baseCredits');
|
||||||
if (total != null && base != null && total > base) return total - base;
|
if (total != null && base != null && total > base) return total - base;
|
||||||
|
|||||||
@ -293,6 +293,21 @@ abstract final class ImageApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 删除任务
|
||||||
|
///
|
||||||
|
/// **Body**(逻辑字段):`taskId`(int,经 [FieldMapping] 映为线网字段,如 FunyMee `taskId` → `exponential`)。
|
||||||
|
/// 与 FunyMee 文档 `POST /v1/image/delete-task` 一致;需登录(自动注入 `User_token`)。
|
||||||
|
static Future<EntityResponse<DeleteTaskResponse>> deleteTask({
|
||||||
|
required int taskId,
|
||||||
|
}) async {
|
||||||
|
return _client.requestEntity(
|
||||||
|
path: '/v1/image/delete-task',
|
||||||
|
method: 'POST',
|
||||||
|
entityFactory: DeleteTaskResponse.fromJson,
|
||||||
|
body: {'taskId': taskId},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取我的任务列表
|
/// 获取我的任务列表
|
||||||
static Future<EntityResponse<MyTasksResponse>> getMyTasks({
|
static Future<EntityResponse<MyTasksResponse>> getMyTasks({
|
||||||
required String app,
|
required String app,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user