Merge branch 'main' of 47.107.44.202:xionglijia/18xchat

This commit is contained in:
2025-08-27 16:12:23 +08:00
10 changed files with 214 additions and 139 deletions
@@ -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);
}
/**
+137
View File
@@ -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();
}
}
}
@@ -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(); // 返回加载的数据
}
}
+12 -42
View File
@@ -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;
}