支付层
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 支付对外的相关接口
|
||||
*
|
||||
*/
|
||||
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<PaymentResult> {
|
||||
// 下单
|
||||
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<void> {
|
||||
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;
|
||||
Reference in New Issue
Block a user