diff --git a/assets/Scripts/chat18x/data/DataManager.ts b/assets/Scripts/chat18x/data/DataManager.ts index 584c6e49..8bd10dd5 100644 --- a/assets/Scripts/chat18x/data/DataManager.ts +++ b/assets/Scripts/chat18x/data/DataManager.ts @@ -11,6 +11,7 @@ import { NetTimeData } from "./NetTimeData"; import { ThemeData } from "./ThemeData"; import { WalletData } from "./WalletData"; import { ShopData } from "./ShopData"; +import { OrderData } from "./OrderData"; /** * 集中定义数据ID,避免写错字符串 @@ -24,6 +25,7 @@ export enum DataId { Theme = "theme", Wallet = "wallet", Shop = "shop", + Order = "order", } /** 构造器类型 */ @@ -57,6 +59,7 @@ export class DataManager { this.register(DataId.Theme, ThemeData); this.register(DataId.Wallet, WalletData); this.register(DataId.Shop, ShopData); + this.register(DataId.Order, OrderData); } /** diff --git a/assets/Scripts/chat18x/data/OrderData.ts b/assets/Scripts/chat18x/data/OrderData.ts new file mode 100644 index 00000000..ee50e211 --- /dev/null +++ b/assets/Scripts/chat18x/data/OrderData.ts @@ -0,0 +1,137 @@ +/** + * 订单数据 + */ +import { BaseData } from "./BaseData"; +import proto from 'db://assets/Scripts/proto/proto.pb.js'; +import type { OrderVO, OrderStatus } from 'db://assets/Scripts/chat18x/payment/types'; +import { mapQueryOrderStatus, shouldPoll } from 'db://assets/Scripts/chat18x/payment/PaymentUtil'; +import { sys } from "cc"; + +const ORDER_STORAGE_KEY = "order_data_v1"; + +export class OrderData extends BaseData { + // 订单列表 + private _orders = new Map(); + + public reset(): void { + this._orders.clear(); + } + + public clear(): void { + this._orders.clear(); + } + + public destroy(): void { + super.destroy(); + this._orders = null; + } + + /** 下单创建:保存 payUrl + orderId,标记处理中 */ + public saveOrderData(res: proto.cs.ICSCreateOrderRes) { + const orderId = res.orderId || ""; + if (!orderId) return; + const vo: OrderVO = { + orderId, // 订单 id + payUrl: res.payUrl || "", // 支付url + status: "CREATED" as OrderStatus, // 订单状态 + retCode: 0, // 失败时的原因码 + createdAt: Date.now(), // 订单创建时间 + }; + this._orders.set(orderId, vo); + console.log("保存订单数据:", this._orders); + } + + /** 根据订单 id 获取订单数据 */ + private getOrderDataById(id: string): OrderVO | null { + if (!this._orders) return null; + return this._orders.get(id) ?? null; + } + + /** 根据订单 id 获取订单状态 */ + public getStatusById(id: string): OrderStatus | null { + let oneData = this.getOrderDataById(id); + if (!oneData) return null; + return oneData.status; + } + + /** 查询结果:更新订单状态 */ + applyQueryOrder(orderId: string, res: proto.cs.ICSQueryOrderRes) { + const vo = this.getOrderDataById(orderId); + if (!vo) return; + // 更新数据 + vo.status = mapQueryOrderStatus(res.status); + vo.retCode = res.retCode ?? 0; + this._orders.set(orderId, vo); + } + + /** 获取所有需要继续轮询查询的订单 id */ + public getAllShouldPollId(): string[] { + const map = this._orders; + const ids: string[] = []; + map.forEach((vo, id) => { + if (shouldPoll(vo.status)) { + ids.push(id); + } + }); + return ids; + } + + /** --------------------------------------------------------- 缓存逻辑 --------------------------------------------------------- */ + + private ensureMap(): Map { + if (!this._orders) { + this._orders = new Map(); + } + return this._orders; + } + + /** 从本地缓存加载订单数据(覆盖内存中的 Map) */ + public loadStorage(): void { + try { + const raw = sys.localStorage.getItem(ORDER_STORAGE_KEY); + if (!raw) { + return; + } + const parsed = JSON.parse(raw) as { v: number; ts: number; orders: OrderVO[] }; + const list = Array.isArray(parsed?.orders) ? parsed.orders : []; + const map = this.ensureMap(); + map.clear(); + for (const vo of list) { + // 简单校验必要字段 + if (vo && vo.orderId) { + map.set(vo.orderId, vo); + } + } + console.log("加载本地订单数据:", map); + } catch (e) { + console.warn("[OrderData] loadStorage parse error:", e); + this.ensureMap().clear(); + } + } + + /** 将当前订单 Map 落盘到本地缓存 */ + public saveStorage(): void { + try { + const map = this.ensureMap(); + const orders: OrderVO[] = Array.from(map.values()); + const payload = { + v: 1, + ts: Date.now(), + orders, + }; + sys.localStorage.setItem(ORDER_STORAGE_KEY, JSON.stringify(payload)); + console.log("保存本地订单数据:", payload); + } catch (e) { + console.warn("[OrderData] saveStorage error:", e); + } + } + + /** 删除本地缓存(同时清空内存中的 Map) */ + public removeStorage(): void { + try { + sys.localStorage.removeItem(ORDER_STORAGE_KEY); + } finally { + this.ensureMap().clear(); + } + } +} diff --git a/assets/Scripts/chat18x/payment/PendingOrderRepo.ts.meta b/assets/Scripts/chat18x/data/OrderData.ts.meta similarity index 70% rename from assets/Scripts/chat18x/payment/PendingOrderRepo.ts.meta rename to assets/Scripts/chat18x/data/OrderData.ts.meta index 8f4deb0f..7aeaf8dd 100644 --- a/assets/Scripts/chat18x/payment/PendingOrderRepo.ts.meta +++ b/assets/Scripts/chat18x/data/OrderData.ts.meta @@ -2,7 +2,7 @@ "ver": "4.0.24", "importer": "typescript", "imported": true, - "uuid": "5165ac21-6ad4-444b-8575-47a851305a31", + "uuid": "caf9897d-17ae-437a-aefb-f923c2c8bd0d", "files": [], "subMetas": {}, "userData": {} diff --git a/assets/Scripts/chat18x/payment/PaymentPoller.ts b/assets/Scripts/chat18x/payment/PaymentPoller.ts index c5c1f150..bc3db85d 100644 --- a/assets/Scripts/chat18x/payment/PaymentPoller.ts +++ b/assets/Scripts/chat18x/payment/PaymentPoller.ts @@ -2,7 +2,6 @@ * 轮询器(指数退避+超时+可取消) * */ - import { PaymentApi } from "./PaymentApi"; import proto from 'db://assets/Scripts/proto/proto.pb.js'; diff --git a/assets/Scripts/chat18x/payment/PaymentService.ts b/assets/Scripts/chat18x/payment/PaymentService.ts index 2328caec..907e0ae3 100644 --- a/assets/Scripts/chat18x/payment/PaymentService.ts +++ b/assets/Scripts/chat18x/payment/PaymentService.ts @@ -5,16 +5,16 @@ import { EventTarget, game, Game } from "cc"; import { PaymentApi } from "./PaymentApi"; import { PaymentOpenerFactory, type OpenStrategy } from "./PaymentOpener"; -import { PendingOrderRepo } from "./PendingOrderRepo"; import PaymentPoller from "./PaymentPoller"; -import type { PaymentResult, PendingOrder, OrderStatus } from "./types"; import proto from 'db://assets/Scripts/proto/proto.pb.js'; +import { DataManager, DataId } from "db://assets/Scripts/chat18x/data/DataManager"; +import { OrderData } from "db://assets/Scripts/chat18x/data/OrderData"; export const PaymentEvents = { Started: "pay-started", // { orderId } UrlOpened: "pay-url-opened", PollTick: "pay-poll-tick", // { orderId, status } - Finished: "pay-finished", // PaymentResult + Finished: "pay-finished", }; export class PaymentService { @@ -41,8 +41,9 @@ export class PaymentService { const { orderId, payUrl } = create.data; this.bus.emit(PaymentEvents.Started, { orderId }); - // 存为未决订单(用于异常恢复) - PendingOrderRepo.instance.add({ orderId, channel: req.channel, createdAt: Date.now(), expireAt, productId: req.productId }); + // 保存订单数据 + const orderData = DataManager.I.getDataById(DataId.Order); + orderData.saveOrderData(create.data); // 打开支付页 await PaymentOpenerFactory.create(open).open(payUrl); @@ -62,7 +63,8 @@ export class PaymentService { jitter: true, }, (d) => this.bus.emit(PaymentEvents.PollTick, { orderId, status: d.status })); - PendingOrderRepo.instance.remove(orderId); + // 刷新订单数据 + orderData.applyQueryOrder(orderId, data); // 返回结果 this.bus.emit(PaymentEvents.Finished, data); return data; @@ -70,45 +72,20 @@ export class PaymentService { /** App 回到前台后恢复未完成订单的轮询(在 onShow 时调用) */ async resumePending(): Promise { - const now = Date.now(); - const list = PendingOrderRepo.instance.all(); - - // 去重:同 orderId 只保留 createdAt 最新的一条 - const uniq = new Map(); - for (const o of list) { - const ex = uniq.get(o.orderId); - if (!ex || (o.createdAt ?? 0) > (ex.createdAt ?? 0)) { - uniq.set(o.orderId, o); - } - } - - for (const o of uniq.values()) { - // 只处理未过期 - if (this.isOrderExpired(o, now)) { - PendingOrderRepo.instance.remove(o.orderId); - this.bus.emit(PaymentEvents.Finished, { - orderId: o.orderId, - status: "EXPIRED" as OrderStatus, - message: "order expired", - } as PaymentResult); - continue; - } - + // 所有需要继续轮询查询的订单 id + const orderData = DataManager.I.getDataById(DataId.Order); + const idList = orderData.getAllShouldPollId(); + for (const o of idList) { // 轮询最新且未过期的订单 - const r = await this.startPollingOnly(o.orderId); + const r = await this.startPollingOnly(o); if (r && r.status !== 1) { - PendingOrderRepo.instance.remove(o.orderId); + orderData.applyQueryOrder(o, r); this.bus.emit(PaymentEvents.Finished, r); } // r === null(轮询超时)时保留在仓库,等待下一次 resume 再查 } } - /** 判断订单是否过期:有 expireAt 且 now >= expireAt 才认为过期 */ - private isOrderExpired(o: PendingOrder, now = Date.now()): boolean { - return typeof o.expireAt === "number" && now >= o.expireAt; - } - /** 仅轮询(用于 resumePending 或手动刷新) */ private async startPollingOnly(orderId: string) { const poller = new PaymentPoller(); diff --git a/assets/Scripts/chat18x/payment/PaymentUtil.ts b/assets/Scripts/chat18x/payment/PaymentUtil.ts new file mode 100644 index 00000000..25f00cda --- /dev/null +++ b/assets/Scripts/chat18x/payment/PaymentUtil.ts @@ -0,0 +1,37 @@ +/** + * 支付/订单相关工具 + */ +import type { OrderStatus } from 'db://assets/Scripts/chat18x/payment/types'; + +/** + * 将数值状态映射为前端字符串状态 + * @param status CSQueryOrderRes.status:-1=失败,0=成功,1=处理中,其它=未知 + * @param options 可选标记(若你的业务在别处判定了取消/过期) + * @returns OrderStatus + */ +export function mapQueryOrderStatus( + status: number | null | undefined, + options: { canceled?: boolean; expired?: boolean } = {} +): OrderStatus { + const { canceled = false, expired = false } = options; + + if (canceled) return "CANCELED"; + if (expired) return "EXPIRED"; + + switch (status) { + case 1: return "PENDING"; // 处理中 + case 0: return "SUCCESS"; // 成功 + case -1: return "FAILED"; // 失败 + default: return "CREATED"; // 未知/初始,按“已创建”兜底 + } +} + +/** 是否为终态(无需再轮询) */ +export function isTerminalStatus(s: OrderStatus): boolean { + return s === "SUCCESS" || s === "FAILED" || s === "CANCELED" || s === "EXPIRED"; +} + +/** 是否应继续轮询查询订单 */ +export function shouldPoll(s: OrderStatus): boolean { + return s === "CREATED" || s === "PENDING"; +} diff --git a/assets/Scripts/chat18x/payment/PaymentUtil.ts.meta b/assets/Scripts/chat18x/payment/PaymentUtil.ts.meta new file mode 100644 index 00000000..bc640150 --- /dev/null +++ b/assets/Scripts/chat18x/payment/PaymentUtil.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "96b89403-6577-455f-a641-d849975a8c89", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/Scripts/chat18x/payment/PendingOrderRepo.ts b/assets/Scripts/chat18x/payment/PendingOrderRepo.ts deleted file mode 100644 index bb946447..00000000 --- a/assets/Scripts/chat18x/payment/PendingOrderRepo.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * 本地持久化未决订单,断线/重启后可恢复 - * - */ - -import { sys } from "cc"; -import type { PendingOrder } from "./types"; - -export class PendingOrderRepo { - private static _instance: PendingOrderRepo | null = null; // 私有静态实例 - private static KEY = "pay:pending_orders"; // 本地存储的 key - private cache: PendingOrder[] | null = null; // 本地缓存 - - // 私有构造函数,禁止外部直接实例化 - private constructor() {} - - // 获取唯一实例 - public static get instance(): PendingOrderRepo { - if (!PendingOrderRepo._instance) { - PendingOrderRepo._instance = new PendingOrderRepo(); - } - return PendingOrderRepo._instance; - } - - // 加载数据 - private load(): PendingOrder[] { - if (this.cache) return this.cache; // 如果有缓存,直接返回 - const raw = sys.localStorage.getItem(PendingOrderRepo.KEY); // 从 localStorage 获取数据 - if (!raw) return (this.cache = []); // 如果没有数据,返回空数组 - try { return (this.cache = JSON.parse(raw) || []); } catch { return (this.cache = []); } // 解析失败时返回空数组 - } - - // 保存数据 - private save(list: PendingOrder[]) { - this.cache = list; // 更新缓存 - sys.localStorage.setItem(PendingOrderRepo.KEY, JSON.stringify(list)); // 存储到 localStorage - } - - // 添加未决订单 - add(o: PendingOrder) { - const list = this.load(); - list.unshift(o); // 将新订单添加到数组前面 - this.save(list.slice(0, 10)); // 保存最多 10 条订单 - } - - // 删除指定订单 - remove(orderId: string) { - const list = this.load().filter(x => x.orderId !== orderId); // 过滤掉指定订单 - this.save(list); // 更新数据 - } - - // 获取所有未决订单 - all(): PendingOrder[] { - return this.load(); // 返回加载的数据 - } -} - diff --git a/assets/Scripts/chat18x/payment/types.ts b/assets/Scripts/chat18x/payment/types.ts index 3d190b8f..88198b9a 100644 --- a/assets/Scripts/chat18x/payment/types.ts +++ b/assets/Scripts/chat18x/payment/types.ts @@ -3,49 +3,19 @@ * */ -export type PaymentChannel = "alipay" | "wxpay" | "stripe" | "paypal" | "url"; - export type OrderStatus = - | "CREATED" // 已创建,待支付 - | "PENDING" // 服务器处理中 - | "SUCCESS" - | "FAILED" - | "CANCELED" - | "EXPIRED"; + | "CREATED" // 已创建,待支付 + | "PENDING" // 服务器处理中 + | "SUCCESS" // 成功 + | "FAILED" // 失败 + | "CANCELED" // 取消 + | "EXPIRED"; // 过期 -// 下单的请求数据 -export interface CreateOrderReq { - productId: string; - amount: number; // 分/元按后端定义 - currency: string; // "CNY" / "USD" ... - channel: PaymentChannel; // “alipay/wxpay/stripe/url”等 - extra?: Record; // 透传(区服、活动、角色信息等) -} - -export interface CreateOrderData { +// 订单数据的数据结构 +export interface OrderVO { orderId: string; - payUrl: string; // 这次集成的核心 - expireAt?: number; // ms -} - -// 查询订单的响应数据 -export interface QueryOrderData { - orderId: string; - status: OrderStatus; - paidAt?: number; // ms - failureReason?: string; -} - -export interface PaymentResult { - orderId: string; - status: OrderStatus; // SUCCESS / FAILED / CANCELED / EXPIRED - message?: string; // 失败原因/提示 -} - -export interface PendingOrder { - orderId: string; - channel: PaymentChannel; - createdAt: number; - expireAt?: number; - productId?: string; + payUrl: string; + status: OrderStatus; + retCode: number; // 失败时的原因码 + createdAt: number; } diff --git a/proto_cs b/proto_cs index 596a85ec..664c8ad7 160000 --- a/proto_cs +++ b/proto_cs @@ -1 +1 @@ -Subproject commit 596a85eceabc1ca62e7ce5404d51df7edfa73a5f +Subproject commit 664c8ad737a91ffcbe862180fefd5a450777ce9a