/** * 支付对外的相关接口 * */ import { EventTarget } from "cc"; import { PaymentApi } from "./PaymentApi"; import { PaymentOpenerFactory, type OpenStrategy } from "./PaymentOpener"; import { PendingOrderRepo } from "./PendingOrderRepo"; import PaymentPoller from "./PaymentPoller"; import type { CreateOrderReq, PaymentResult, PendingOrder, OrderStatus } from "./types"; import { ApiCode } from "../network/client/types"; export const PaymentEvents = { Started: "pay-started", // { orderId } UrlOpened: "pay-url-opened", PollTick: "pay-poll-tick", // { orderId, status } Finished: "pay-finished", // PaymentResult }; export class PaymentService { private static _I: PaymentService | null = null; static get I() { return this._I ?? (this._I = new PaymentService()); } private constructor() {} private bus = new EventTarget(); on(event: string, cb: (...args: any[]) => void, target?: any) { this.bus.on(event, cb, target); } off(event: string, cb: (...args: any[]) => void, target?: any) { this.bus.off(event, cb, target); } /** 发起支付:创建订单→打开URL→回到App后开始轮询 */ async startPayment(req: CreateOrderReq, open: OpenStrategy = "system"): Promise { // 下单 const create = await PaymentApi.I.createOrder(req); if (create.code !== ApiCode.OK || !create.data) { return { orderId: "", status: "FAILED", message: create.msg || "create order failed" }; } const { orderId, payUrl, expireAt } = create.data; this.bus.emit(PaymentEvents.Started, { orderId }); // 存为未决订单(用于异常恢复) PendingOrderRepo.instance.add({ orderId, channel: req.channel, createdAt: Date.now(), expireAt, productId: req.productId }); // 打开支付页 await PaymentOpenerFactory.create(open).open(payUrl); this.bus.emit(PaymentEvents.UrlOpened, { orderId }); // 等待回到前台(系统浏览器策略下) // - 如果是 inapp/minigame,可在 WebView/回跳时立即开始轮询; // - 这里给一个通用的“等待前台”方法(可用 Cocos 的 onShow/onHide 自行接入) await this.waitAppResumeIfNeeded(open); // 轮询 const poller = new PaymentPoller(); const data = await poller.poll(orderId, { maxDurationMs: 2 * 60 * 1000, startIntervalMs: 2000, maxIntervalMs: 10000, jitter: true, }, (d) => this.bus.emit(PaymentEvents.PollTick, { orderId, status: d.status })); // 产出结果 let res: PaymentResult; if (!data) { res = { orderId, status: "EXPIRED", message: "poll timeout" }; } else if (data.status === "SUCCESS") { res = { orderId, status: "SUCCESS" }; } else { res = { orderId, status: data.status as OrderStatus, message: data.failureReason }; } PendingOrderRepo.instance.remove(orderId); this.bus.emit(PaymentEvents.Finished, res); return res; } /** App 回到前台后恢复未完成订单的轮询(在 onShow 时调用) */ async resumePending(): Promise { const all = PendingOrderRepo.instance.all(); for (const o of all) { // 可以做去重/只处理未过期的 const r = await this.startPollingOnly(o.orderId); if (r && r.status !== "PENDING" && r.status !== "CREATED") { PendingOrderRepo.instance.remove(o.orderId); this.bus.emit(PaymentEvents.Finished, { orderId: o.orderId, status: r.status as OrderStatus, message: r.failureReason } as PaymentResult); } } } /** 仅轮询(用于 resumePending 或手动刷新) */ private async startPollingOnly(orderId: string) { const poller = new PaymentPoller(); return poller.poll(orderId, { maxDurationMs: 2 * 60 * 1000, startIntervalMs: 2000, maxIntervalMs: 8000, jitter: true, }); } /** 等待“回到前台”的占位实现:实际请在 App 生命周期里调用 resumePending() */ private async waitAppResumeIfNeeded(open: OpenStrategy) { // 如果是系统浏览器,我们通常等“App 回到前台”再开始轮询; // 这里简单 sleep 1s,实际项目请监听 Cocos 的 onShow/onHide 或平台回调。 if (open === "system") { await new Promise(res => setTimeout(res, 1000)); } } } export default PaymentService;