支付协议
This commit is contained in:
@@ -11,6 +11,7 @@ import { NetTimeData } from "./NetTimeData";
|
|||||||
import { ThemeData } from "./ThemeData";
|
import { ThemeData } from "./ThemeData";
|
||||||
import { WalletData } from "./WalletData";
|
import { WalletData } from "./WalletData";
|
||||||
import { ShopData } from "./ShopData";
|
import { ShopData } from "./ShopData";
|
||||||
|
import { OrderData } from "./OrderData";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 集中定义数据ID,避免写错字符串
|
* 集中定义数据ID,避免写错字符串
|
||||||
@@ -24,6 +25,7 @@ export enum DataId {
|
|||||||
Theme = "theme",
|
Theme = "theme",
|
||||||
Wallet = "wallet",
|
Wallet = "wallet",
|
||||||
Shop = "shop",
|
Shop = "shop",
|
||||||
|
Order = "order",
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造器类型 */
|
/** 构造器类型 */
|
||||||
@@ -57,6 +59,7 @@ export class DataManager {
|
|||||||
this.register(DataId.Theme, ThemeData);
|
this.register(DataId.Theme, ThemeData);
|
||||||
this.register(DataId.Wallet, WalletData);
|
this.register(DataId.Wallet, WalletData);
|
||||||
this.register(DataId.Shop, ShopData);
|
this.register(DataId.Shop, ShopData);
|
||||||
|
this.register(DataId.Order, OrderData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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<string, OrderVO>();
|
||||||
|
|
||||||
|
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<string, OrderVO> {
|
||||||
|
if (!this._orders) {
|
||||||
|
this._orders = new Map<string, OrderVO>();
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"ver": "4.0.24",
|
"ver": "4.0.24",
|
||||||
"importer": "typescript",
|
"importer": "typescript",
|
||||||
"imported": true,
|
"imported": true,
|
||||||
"uuid": "5165ac21-6ad4-444b-8575-47a851305a31",
|
"uuid": "caf9897d-17ae-437a-aefb-f923c2c8bd0d",
|
||||||
"files": [],
|
"files": [],
|
||||||
"subMetas": {},
|
"subMetas": {},
|
||||||
"userData": {}
|
"userData": {}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
* 轮询器(指数退避+超时+可取消)
|
* 轮询器(指数退避+超时+可取消)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PaymentApi } from "./PaymentApi";
|
import { PaymentApi } from "./PaymentApi";
|
||||||
import proto from 'db://assets/Scripts/proto/proto.pb.js';
|
import proto from 'db://assets/Scripts/proto/proto.pb.js';
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,16 @@
|
|||||||
import { EventTarget, game, Game } from "cc";
|
import { EventTarget, game, Game } from "cc";
|
||||||
import { PaymentApi } from "./PaymentApi";
|
import { PaymentApi } from "./PaymentApi";
|
||||||
import { PaymentOpenerFactory, type OpenStrategy } from "./PaymentOpener";
|
import { PaymentOpenerFactory, type OpenStrategy } from "./PaymentOpener";
|
||||||
import { PendingOrderRepo } from "./PendingOrderRepo";
|
|
||||||
import PaymentPoller from "./PaymentPoller";
|
import PaymentPoller from "./PaymentPoller";
|
||||||
import type { PaymentResult, PendingOrder, OrderStatus } from "./types";
|
|
||||||
import proto from 'db://assets/Scripts/proto/proto.pb.js';
|
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 = {
|
export const PaymentEvents = {
|
||||||
Started: "pay-started", // { orderId }
|
Started: "pay-started", // { orderId }
|
||||||
UrlOpened: "pay-url-opened",
|
UrlOpened: "pay-url-opened",
|
||||||
PollTick: "pay-poll-tick", // { orderId, status }
|
PollTick: "pay-poll-tick", // { orderId, status }
|
||||||
Finished: "pay-finished", // PaymentResult
|
Finished: "pay-finished",
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PaymentService {
|
export class PaymentService {
|
||||||
@@ -41,8 +41,9 @@ export class PaymentService {
|
|||||||
const { orderId, payUrl } = create.data;
|
const { orderId, payUrl } = create.data;
|
||||||
this.bus.emit(PaymentEvents.Started, { orderId });
|
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<OrderData>(DataId.Order);
|
||||||
|
orderData.saveOrderData(create.data);
|
||||||
|
|
||||||
// 打开支付页
|
// 打开支付页
|
||||||
await PaymentOpenerFactory.create(open).open(payUrl);
|
await PaymentOpenerFactory.create(open).open(payUrl);
|
||||||
@@ -62,7 +63,8 @@ export class PaymentService {
|
|||||||
jitter: true,
|
jitter: true,
|
||||||
}, (d) => this.bus.emit(PaymentEvents.PollTick, { orderId, status: d.status }));
|
}, (d) => this.bus.emit(PaymentEvents.PollTick, { orderId, status: d.status }));
|
||||||
|
|
||||||
PendingOrderRepo.instance.remove(orderId);
|
// 刷新订单数据
|
||||||
|
orderData.applyQueryOrder(orderId, data);
|
||||||
// 返回结果
|
// 返回结果
|
||||||
this.bus.emit(PaymentEvents.Finished, data);
|
this.bus.emit(PaymentEvents.Finished, data);
|
||||||
return data;
|
return data;
|
||||||
@@ -70,45 +72,20 @@ export class PaymentService {
|
|||||||
|
|
||||||
/** App 回到前台后恢复未完成订单的轮询(在 onShow 时调用) */
|
/** App 回到前台后恢复未完成订单的轮询(在 onShow 时调用) */
|
||||||
async resumePending(): Promise<void> {
|
async resumePending(): Promise<void> {
|
||||||
const now = Date.now();
|
// 所有需要继续轮询查询的订单 id
|
||||||
const list = PendingOrderRepo.instance.all();
|
const orderData = DataManager.I.getDataById<OrderData>(DataId.Order);
|
||||||
|
const idList = orderData.getAllShouldPollId();
|
||||||
// 去重:同 orderId 只保留 createdAt 最新的一条
|
for (const o of idList) {
|
||||||
const uniq = new Map<string, PendingOrder>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 轮询最新且未过期的订单
|
// 轮询最新且未过期的订单
|
||||||
const r = await this.startPollingOnly(o.orderId);
|
const r = await this.startPollingOnly(o);
|
||||||
if (r && r.status !== 1) {
|
if (r && r.status !== 1) {
|
||||||
PendingOrderRepo.instance.remove(o.orderId);
|
orderData.applyQueryOrder(o, r);
|
||||||
this.bus.emit(PaymentEvents.Finished, r);
|
this.bus.emit(PaymentEvents.Finished, r);
|
||||||
}
|
}
|
||||||
// r === null(轮询超时)时保留在仓库,等待下一次 resume 再查
|
// r === null(轮询超时)时保留在仓库,等待下一次 resume 再查
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断订单是否过期:有 expireAt 且 now >= expireAt 才认为过期 */
|
|
||||||
private isOrderExpired(o: PendingOrder, now = Date.now()): boolean {
|
|
||||||
return typeof o.expireAt === "number" && now >= o.expireAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 仅轮询(用于 resumePending 或手动刷新) */
|
/** 仅轮询(用于 resumePending 或手动刷新) */
|
||||||
private async startPollingOnly(orderId: string) {
|
private async startPollingOnly(orderId: string) {
|
||||||
const poller = new PaymentPoller();
|
const poller = new PaymentPoller();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "96b89403-6577-455f-a641-d849975a8c89",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -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(); // 返回加载的数据
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -3,49 +3,19 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type PaymentChannel = "alipay" | "wxpay" | "stripe" | "paypal" | "url";
|
|
||||||
|
|
||||||
export type OrderStatus =
|
export type OrderStatus =
|
||||||
| "CREATED" // 已创建,待支付
|
| "CREATED" // 已创建,待支付
|
||||||
| "PENDING" // 服务器处理中
|
| "PENDING" // 服务器处理中
|
||||||
| "SUCCESS"
|
| "SUCCESS" // 成功
|
||||||
| "FAILED"
|
| "FAILED" // 失败
|
||||||
| "CANCELED"
|
| "CANCELED" // 取消
|
||||||
| "EXPIRED";
|
| "EXPIRED"; // 过期
|
||||||
|
|
||||||
// 下单的请求数据
|
// 订单数据的数据结构
|
||||||
export interface CreateOrderReq {
|
export interface OrderVO {
|
||||||
productId: string;
|
|
||||||
amount: number; // 分/元按后端定义
|
|
||||||
currency: string; // "CNY" / "USD" ...
|
|
||||||
channel: PaymentChannel; // “alipay/wxpay/stripe/url”等
|
|
||||||
extra?: Record<string, any>; // 透传(区服、活动、角色信息等)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateOrderData {
|
|
||||||
orderId: string;
|
orderId: string;
|
||||||
payUrl: string; // 这次集成的核心
|
payUrl: string;
|
||||||
expireAt?: number; // ms
|
status: OrderStatus;
|
||||||
}
|
retCode: number; // 失败时的原因码
|
||||||
|
createdAt: number;
|
||||||
// 查询订单的响应数据
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule proto_cs updated: 596a85ecea...664c8ad737
Reference in New Issue
Block a user