diff --git a/docs/credit_record_requirements_and_implementation.md b/docs/credit_record_requirements_and_implementation.md new file mode 100644 index 0000000..5b0a73e --- /dev/null +++ b/docs/credit_record_requirements_and_implementation.md @@ -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` 语义一致。 \ No newline at end of file diff --git a/docs/history_page_requirements_and_implementation.md b/docs/history_page_requirements_and_implementation.md new file mode 100644 index 0000000..990393c --- /dev/null +++ b/docs/history_page_requirements_and_implementation.md @@ -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` 判定稳定。 + diff --git a/docs/payment_flow.md b/docs/payment_flow.md index 29025ad..9c5de4c 100644 --- a/docs/payment_flow.md +++ b/docs/payment_flow.md @@ -1,298 +1,198 @@ -# 支付流程文档 +# 支付流程(多客户端通用) ## 1. 概述 -本文档描述 Android 项目(Flutter)的完整支付流程,包括商品获取、支付方式选择、订单创建、Google Play 内购以及补单机制。 +本文档描述换皮应用可复用的支付全流程,覆盖:商品获取、支付方式选择、订单创建、平台内购回调、订单核销、补单与账户刷新。 +文档以“能力与流程”表达,不绑定具体客户端项目、目录路径或某一语言实现。 --- ## 2. 支付流程总览 -``` -用户点击 Buy +```text +用户点击充值 / Buy │ - ├─ enableThirdPartyPayment === true 且已登录 + ├─ 已登录 且 开启第三方支付 │ │ - │ ├─ getPaymentMethods(activityId) 获取支付方式 - │ ├─ 弹窗选择支付方式 - │ ├─ createPayment 创建订单 + │ ├─ 拉取支付方式(基于商品/活动) + │ ├─ 用户选择支付方式 + │ ├─ 创建订单(服务端生成业务订单) │ │ - │ ├─ 若选中的是 Google Pay - │ │ ├─ 调起 Google Play 内购 - │ │ ├─ 拿到 purchaseData + signature - │ │ └─ googlepay 回调验证 + │ ├─ 若为平台内购(如 Google Play) + │ │ ├─ 调起商店购买 + │ │ ├─ 获取 purchaseData + signature + storeOrderId + │ │ └─ 回调服务端校验与入账 │ │ - │ └─ 否则(其他支付方式) - │ └─ 打开 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.1 接口 +### 4.1 获取时机 -```dart -// Android(app 默认 backendAppTypeAndroid,可传 client) -final res = await PaymentApi.getGooglePayActivities( - country: '国家', // 可选 -); +- 进入充值页时拉取一次; +- 下拉刷新或切换国家/渠道后重新拉取; +- 支付成功后建议刷新,确保档位与余额展示一致。 -// iOS(app 默认 backendAppTypeIOS) -final res = await PaymentApi.getApplePayActivities( - country: '国家', // 可选 -); -``` +### 4.2 商品字段(通用语义) -### 4.2 返回实体 - -```dart -class PaymentProductsResponse { - List? productList; -} - -class PaymentProductItem { - String? productId; // 商品 ID (对应 helm) - String? activityId; // 活动 ID (对应 warrior) - String? actualAmount; // 实际金额 (对应 guardian) - String? originAmount; // 原价 (对应 curriculum) - int? bonus; // 赠送积分 (对应 forge) - String? title; // 标题 (对应 glossary) -} -``` +| 字段语义 | 说明 | +|------|------| +| `productId` | 商店商品 ID(内购必填) | +| `activityId` | 服务端活动/档位 ID(建单常用) | +| `actualAmount` | 实付金额 | +| `originAmount` | 原价(可用于划线价) | +| `bonus` / `bonusCredits` | 赠送积分 | +| `credits` | 到账积分(如与赠送分开展示) | +| `title` | 商品文案(可能由服务端直接下发) | --- -## 5. 第三方支付流程 +## 5. 第三方支付流程(通用) -### 5.1 获取支付方式 +### 5.1 拉取支付方式 -```dart -final res = await PaymentApi.getPaymentMethods( - activityId: 12345, // int - country: '国家', // 可选 -); +以商品或活动为上下文请求支付方式列表,常见字段包括: -if (res.isSuccess) { - final methods = res.data!.paymentMethods; - // methods 包含: - // - paymentMethod: 支付方式 (如 GOOGLEPAY) - // - subPaymentMethod: 子支付方式 - // - name: 显示名称 - // - icon: 图标 URL - // - recommend: 是否推荐 -} -``` +- `paymentMethod`:主支付方式(例如平台内购、钱包、卡支付等) +- `subPaymentMethod`:子方式(可选) +- `name` / `icon`:展示信息 +- `recommend`:是否推荐 ### 5.2 创建订单 -```dart -final res = await PaymentApi.createPayment( - app: 'HAndroid', // HIOS / HAndroid - userId: '用户ID', - activityId: '活动ID', - paymentMethod: '支付方式', - paymentType: '支付子类型', // 可选 -); +创建订单请求通常包含: -if (res.isSuccess) { - final order = res.data!; - final orderId = order.orderId; // 订单 ID - final payUrl = order.payUrl; // 支付链接 -} -``` +- 应用标识(`app`) +- 用户标识(`userId`) +- 商品或活动标识(`activityId` / `productId`) +- 支付方式(`paymentMethod`、可选 `paymentType`) -### 5.3 支付分支 +响应通常返回: -- **Google Pay**: 调起内购 → 获取 purchaseData/signature → 调用 `googlepay` 回调 -- **其他方式**: 打开 `payUrl` 在外部浏览器 +- `orderId`(业务订单号) +- `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) { - // result.orderId: Google 订单号 - // result.payload.purchaseData: 用于 merchant - // result.payload.signature: 用于 signature - // result.purchaseDetails: PurchaseDetails 对象 -} -``` +### 7.2 恢复流程 -### 7.2 回调验证 - -```dart -final res = await PaymentApi.googlepay( - signature: result.payload.signature, - 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; // 是否已加积分 -} -``` +1. 拉取未完成/未核销的商店订单(历史订单 + 监听流中的待处理订单); +2. 对每笔订单尝试找到业务订单映射(如 `federation` / `orderId`); +3. 若可映射:再次走服务端回调核验,成功后完成/消费交易; +4. 若不可映射:按产品策略处理(仅完成/消费以解除占用,或进入人工排查队列); +5. 恢复结束后刷新账户与充值记录。 --- -## 8. 补单机制 +## 8. API 能力清单(通用命名) -### 8.1 触发时机 - -- 进入充值页时调用 `GooglePlayPurchaseService.runOrderRecovery()` - -### 8.2 补单流程 - -1. 获取未核销订单: `getUnacknowledgedPurchases()` - - 合并 `queryPastPurchases` 和 `purchaseStream` 的待处理订单 - -2. 对每笔订单: - - 查询本地存储的 `federation` 映射 - - 若存在 federation: 调用 `googlepay` 回调 → 成功后 consume - - 若无 federation: 仅执行 consume 解除「已拥有此内容」 - -3. 补单成功后刷新账户 +| 能力 | 典型方法 | 说明 | +|------|------|------| +| 获取商品列表 | `GET` | Android/iOS 可分端点 | +| 获取支付方式 | `POST`/`GET` | 入参通常含 `activityId` | +| 创建订单 | `POST` | 返回 `orderId`、`payUrl` 等 | +| 平台内购回调 | `POST` | 提交 `purchaseData`、`signature` 等 | +| 订单列表 | `GET` | 历史支付记录 | +| 订单详情 | `GET` | 查询单笔订单状态 | --- -## 9. API 汇总 +## 9. 核心字段对照(通用) -| 接口 | 方法 | 返回实体 | -|------|------|----------| -| `getGooglePayActivities` | GET | `PaymentProductsResponse` | -| `getApplePayActivities` | GET | `PaymentProductsResponse` | -| `getPaymentMethods` | POST | `PaymentMethodsResponse` | -| `createPayment` | POST | `CreatePaymentResponse` | -| `getPaymentDetailList` | GET | `PaymentOrderListResponse` | -| `getOrderDetail` | GET | `OrderDetailResponse` | -| `googlepay` | POST | `GooglePayCallbackResponse` | +| 业务含义 | 常见请求字段 | 常见响应字段 | +|------|------|------| +| 应用标识 | `app` | - | +| 用户标识 | `userId` | - | +| 活动/档位 | `activityId` | - | +| 支付方式 | `paymentMethod` | - | +| 子支付方式 | `paymentType` | - | +| 订单标识 | `orderId` / `id` | `orderId` | +| 购买签名 | `signature` | - | +| 购买数据 | `purchaseData` | - | +| 商品 ID | - | `productId` | +| 实付金额 | - | `actualAmount` | +| 原价 | - | `originAmount` | +| 赠送积分 | - | `bonus` / `bonusCredits` | +| 支付链接 | - | `payUrl` | +| 订单状态 | - | `status` | --- -## 10. 字段映射说明 +## 10. 常见问题 -框架自动完成字段映射,调用层使用原始字段名。 +### 10.1 商品未找到 -### 请求 → 响应 字段对照 +- **现象**:发起内购时提示商品不存在; +- **常见原因**:客户端 `productId` 与商店后台配置不一致,或商品未上架到当前测试轨道; +- **排查建议**:核对包名/应用 ID、商品 ID、地区、测试账号与轨道配置。 -| 业务含义 | 请求字段 | 响应字段 | -|----------|----------|----------| -| 应用 ID | app | - | -| 用户 ID | userId | - | -| 活动 ID | activityId | - | -| 支付方式 | paymentMethod | - | -| 支付子类型 | paymentType | - | -| 订单 / 支付 ID(详情 query) | `id`(Dart 仍用参数名 orderId) | orderId | -| 购买签名 | signature | - | -| 购买数据 | purchaseData | - | -| 商品 ID | - | productId | -| 实际金额 | - | actualAmount | -| 原价 | - | originAmount | -| 赠送积分 | - | bonus | -| 支付链接 | - | payUrl | -| 订单状态 | - | status | +### 10.2 回调成功但积分未更新 + +- **现象**:支付侧显示成功,账户余额未变; +- **常见原因**:回调后未刷新账户、订单状态未落库、幂等冲突; +- **排查建议**:检查回调响应、订单状态接口与账户刷新链路。 + +### 10.3 重复购买被拦截(已拥有此内容) + +- **现象**:商店提示已拥有,无法再次购买; +- **常见原因**:上一笔交易未完成/未消费; +- **排查建议**:执行补单恢复流程并确保完成/消费逻辑成功。 --- -## 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 秒 diff --git a/lib/src/entities/image_entities.dart b/lib/src/entities/image_entities.dart index 2ee242b..f2a99e8 100644 --- a/lib/src/entities/image_entities.dart +++ b/lib/src/entities/image_entities.dart @@ -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 json) { + return DeleteTaskResponse( + hint: json['hint'] as String?, + type: json['type'] as String?, + ); + } + + @override + Map toJson() => { + 'hint': hint, + 'type': type, + }; +} + /// 我的任务项 /// /// 与 FunyMee 文档一致:`state`(线网 `bitrate`)1–6 为权威任务态;`status` 为兼容字段。 diff --git a/lib/src/entities/payment_entities.dart b/lib/src/entities/payment_entities.dart index 34e7a3a..464c531 100644 --- a/lib/src/entities/payment_entities.dart +++ b/lib/src/entities/payment_entities.dart @@ -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 json) { final direct = _intField(json, 'bonusCredits'); if (direct != null && direct > 0) return direct; final add = _intField(json, 'addCredits'); 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 base = _intField(json, 'baseCredits'); if (total != null && base != null && total > base) return total - base; diff --git a/lib/src/services/image_api.dart b/lib/src/services/image_api.dart index 3b19b59..7f6def5 100644 --- a/lib/src/services/image_api.dart +++ b/lib/src/services/image_api.dart @@ -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> deleteTask({ + required int taskId, + }) async { + return _client.requestEntity( + path: '/v1/image/delete-task', + method: 'POST', + entityFactory: DeleteTaskResponse.fromJson, + body: {'taskId': taskId}, + ); + } + /// 获取我的任务列表 static Future> getMyTasks({ required String app,