Merge branch 'main' of 47.107.44.202:xionglijia/18xchat
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5165ac21-6ad4-444b-8575-47a851305a31",
|
||||
"uuid": "caf9897d-17ae-437a-aefb-f923c2c8bd0d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
@@ -2,7 +2,6 @@
|
||||
* 轮询器(指数退避+超时+可取消)
|
||||
*
|
||||
*/
|
||||
|
||||
import { PaymentApi } from "./PaymentApi";
|
||||
import proto from 'db://assets/Scripts/proto/proto.pb.js';
|
||||
|
||||
|
||||
@@ -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<OrderData>(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<void> {
|
||||
const now = Date.now();
|
||||
const list = PendingOrderRepo.instance.all();
|
||||
|
||||
// 去重:同 orderId 只保留 createdAt 最新的一条
|
||||
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;
|
||||
}
|
||||
|
||||
// 所有需要继续轮询查询的订单 id
|
||||
const orderData = DataManager.I.getDataById<OrderData>(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();
|
||||
|
||||
@@ -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 =
|
||||
| "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<string, any>; // 透传(区服、活动、角色信息等)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
+1
-1
Submodule proto_cs updated: 596a85ecea...664c8ad737
Reference in New Issue
Block a user