This commit is contained in:
2025-08-22 15:06:36 +08:00
18 changed files with 1229 additions and 292 deletions
+20 -4
View File
@@ -33,11 +33,10 @@ import { NetTimeData } from "../../chat18x/data/NetTimeData";
import ConfigManager from "../../chat18x/manager/ConfigManager";
import { AppConfig } from "db://assets/Scripts/chat18x/config/env/appConfig";
import DeviceIdService from "db://assets/Scripts/chat18x/foundation/identity/DeviceIdService";
import { AuthService } from "db://assets/Scripts/chat18x/network/services/AuthService";
import { ApiCode } from "db://assets/Scripts/chat18x/network/client/types";
import MachineInfoService from "db://assets/Scripts/chat18x/foundation/identity/MachineInfoService";
import { AuthService } from "db://assets/Scripts/chat18x/network/services/AuthService";
import { PlayerDataService } from "db://assets/Scripts/chat18x/network/services/PlayerDataService";
import { ApiCode } from "db://assets/Scripts/chat18x/network/client/types";
const { ccclass, property } = _decorator;
@@ -295,9 +294,26 @@ export class PreloadUI extends Component {
netTimeData.timezoneOffset = resData.TimezoneOffset;
this.loginFinish = true;
// this.reqPlayerData();
};
}
// 请求玩家数据
private async reqPlayerData() {
const reqData = {
GameName: ConfigManager.tables.TbGlobalConfig.GameName,
IsAll: true,
DataFlagList: [],
};
console.log("玩家数据请求数据:", reqData);
let res = await PlayerDataService.I.reqQueryPlayerData(reqData);
console.log("玩家数据响应数据:", res);
if (res && res.code === ApiCode.OK) {
}
}
enterGame() {
//let islogin = HttpUnit.IsLogin();
//console.log("进入游戏,登录状态:", islogin);
@@ -13,12 +13,12 @@ export class AuthService {
// 登录
public async login(req: proto.ProtoMsg.ILoginReq): Promise<ApiResponse<proto.ProtoMsg.ILoginRsp>> {
let EP_LOGIN: Endpoint<proto.ProtoMsg.ILoginReq, proto.ProtoMsg.ILoginRsp> = {
let epData: Endpoint<proto.ProtoMsg.ILoginReq, proto.ProtoMsg.ILoginRsp> = {
path: "login",
method: "POST",
codec: "json",
needsAuth: false
};
return this.api.call(EP_LOGIN, req);
};
return this.api.call(epData, req);
}
}
@@ -0,0 +1,35 @@
import { ApiClient } from "../client/ApiClient";
import type { Endpoint } from "../client/endpoints";
import type { ApiResponse } from "../client/types";
import proto from 'db://assets/Scripts/proto/proto.pb.js';
export class PlayerDataService {
private static _I: PlayerDataService | null = null;
public static get I(): PlayerDataService {
if (!PlayerDataService._I) PlayerDataService._I = new PlayerDataService();
return PlayerDataService._I;
}
private constructor(private api = ApiClient.I) {}
// 拉取玩家数据
public async reqQueryPlayerData(req: proto.ProtoMsg.IQueryPlayerDataReq): Promise<ApiResponse<proto.ProtoMsg.IQueryPlayerDataRsp>> {
let epData: Endpoint<proto.ProtoMsg.IQueryPlayerDataReq, proto.ProtoMsg.IQueryPlayerDataRsp> = {
path: "playerdata/query_player_data",
method: "POST",
codec: "json",
needsAuth: true
};
return this.api.call(epData, req);
}
// 保存玩家数据
public async reqSavePlayerData(req: proto.ProtoMsg.ISavePlayerDataReq): Promise<ApiResponse<proto.ProtoMsg.ISavePlayerDataRsp>> {
let epData: Endpoint<proto.ProtoMsg.ISavePlayerDataReq, proto.ProtoMsg.ISavePlayerDataRsp> = {
path: "playerdata/save_player_data",
method: "POST",
codec: "json",
needsAuth: true
};
return this.api.call(epData, req);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e651acda-1f16-4ceb-88e4-be892c13a248",
"files": [],
"subMetas": {},
"userData": {}
}
+9
View File
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3da5f7b4-77ad-4f88-959b-624499248ae0",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,48 @@
/**
* 和服务器交互的API接口
*
*/
import { ApiClient } from "../network/client/ApiClient";
import type { Endpoint } from "../network/client/endpoints";
import type { ApiResponse } from "../network/client/types";
import { OrderStatus, CreateOrderReq, CreateOrderData, QueryOrderData } from "./types";
import proto from 'db://assets/Scripts/proto/proto.pb.js';
export class PaymentApi {
private static _I: PaymentApi | null = null;
static get I() { return this._I ?? (this._I = new PaymentApi()); }
private constructor(private api = ApiClient.I) {}
// 下单
createOrder(req: CreateOrderReq): Promise<ApiResponse<CreateOrderData>> {
let epData: Endpoint<CreateOrderReq, { orderId: string; payUrl: string; expireAt?: number; }> = {
path: "pay/create",
method: "POST",
codec: "json",
needsAuth: true,
};
return this.api.call(epData, req);
}
// 查询订单
queryOrder(orderId: string): Promise<ApiResponse<QueryOrderData>> {
let epData: Endpoint<{ orderId: string }, { orderId: string; status: OrderStatus; paidAt?: number; failureReason?: string; }> = {
path: "pay/query",
method: "POST",
codec: "json",
needsAuth: true,
};
return this.api.call(epData, { orderId });
}
// 取消订单
cancelOrder(orderId: string): Promise<ApiResponse<{ ok: boolean }>> {
let epData: Endpoint<{ orderId: string }, { ok: boolean; }> = {
path: "pay/cancel",
method: "POST",
codec: "json",
needsAuth: true,
};
return this.api.call(epData, { orderId });
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c4e3bad2-2588-427a-ba4f-9d839205c37d",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,54 @@
/**
* 打开支付URL(系统浏览器 / 内嵌 WebView / 小游戏 API),可插拔
*
*/
import { sys } from "cc";
export type OpenStrategy = "system" | "inapp" | "minigame";
export interface IPaymentOpener {
open(url: string): Promise<void>;
canReturnSignal(): boolean; // 是否能“自动感知回到App”(inapp/minigame一般可以)
}
/** 系统浏览器:使用 sys.openURL。无法自动知道用户何时完成,靠“回到前台”事件触发轮询 */
class SystemBrowserOpener implements IPaymentOpener {
async open(url: string) { sys.openURL(url); }
canReturnSignal() { return false; }
}
// 如需内嵌 WebView,可封装一个组件并在 open 里显示;此处给空壳
class InAppWebViewOpener implements IPaymentOpener {
async open(url: string) {
// TODO: 实现你的 WebView 弹窗并加载 url;可拦截 redirect_uri 关闭弹窗
sys.openURL(url); // 临时:用系统浏览器兜底
}
canReturnSignal() { return true; } // 拦截回跳时可立即开始轮询
}
// 小游戏:使用平台API打开/内嵌webview,回跳后触发事件
class MiniGameOpener implements IPaymentOpener {
async open(url: string) {
const g: any = globalThis as any;
if (g.tt?.openAwemeUserProfile) {
// 示例:按实际平台API替换
g.tt.openSchema({ schema: url });
} else if (g.wx?.openUrl) {
g.wx.openUrl({ url });
} else {
sys.openURL(url);
}
}
canReturnSignal() { return true; }
}
export class PaymentOpenerFactory {
static create(strategy: OpenStrategy): IPaymentOpener {
switch (strategy) {
case "inapp": return new InAppWebViewOpener();
case "minigame": return new MiniGameOpener();
default: return new SystemBrowserOpener();
}
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "d1bfe39e-0c3f-4c88-ac5d-510ee01c9121",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,57 @@
/**
* 轮询器(指数退避+超时+可取消)
*
*/
import { PaymentApi } from "./PaymentApi";
import type { QueryOrderData } from "./types";
import { ApiCode } from "../network/client/types";
export interface PollOptions {
maxDurationMs: number; // 轮询总时长(例如 2分钟)
startIntervalMs: number; // 初始间隔(例如 2s
maxIntervalMs: number; // 最大间隔(例如 10s
jitter?: boolean; // 抖动
}
export class PaymentPoller {
private cancelled = false;
// 取消轮询
cancel() { this.cancelled = true; }
// 轮询订单
async poll(orderId: string, opt: PollOptions, onTick?: (d: QueryOrderData) => void): Promise<QueryOrderData | null> {
const t0 = Date.now();
let interval = opt.startIntervalMs;
while (!this.cancelled) {
// 查询
const r = await PaymentApi.I.queryOrder(orderId);
if (r.code === ApiCode.OK && r.data) {
// 回调
onTick?.(r.data);
if (["SUCCESS", "FAILED", "CANCELED", "EXPIRED"].indexOf(r.data.status) !== -1) {
return r.data;
}
}
// 退出条件
if (Date.now() - t0 > opt.maxDurationMs) return null;
// 等待
await this.sleep(this.jitter(interval, opt));
// 每次轮询后,interval 会按照 1.5 倍增长,直到达到最大间隔 maxIntervalMs,避免在短时间内重复请求
interval = Math.min(interval * 1.5, opt.maxIntervalMs);
}
return null;
}
// 用来暂停一定的时间(ms 毫秒),并让轮询进入等待状态
private sleep(ms: number) { return new Promise(res => setTimeout(res, ms)); }
// 用于实现抖动(随机化轮询间隔)
private jitter(base: number, opt: PollOptions) {
if (!opt.jitter) return base;
const delta = Math.min(500, Math.max(100, base * 0.1));
return base + (Math.random() * 2 - 1) * delta;
}
}
export default PaymentPoller;
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a5127d19-3cee-4a5f-9ff9-d602dbb48f09",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,167 @@
/**
* 支付对外的相关接口
*
*/
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 { 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 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;
}
// 轮询最新且未过期的订单
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);
}
// 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();
return poller.poll(orderId, {
maxDurationMs: 2 * 60 * 1000,
startIntervalMs: 2000,
maxIntervalMs: 8000,
jitter: true,
});
}
/** 等待“回到前台”的占位实现:实际请在 App 生命周期里调用 resumePending() */
private waitAppResumeIfNeeded(open: OpenStrategy): Promise<void> {
if (open !== "system") {
// 内嵌 WebView / 小游戏平台通常可以在回调里直接开始轮询
return Promise.resolve();
}
return new Promise<void>((resolve) => {
let finished = false;
const finish = () => {
if (finished) return;
finished = true;
// 安全移除监听
game.off(Game.EVENT_SHOW, onShow, this);
resolve();
};
const onShow = () => {
// 应用回到前台
finish();
};
// 监听一次“回到前台”
game.on(Game.EVENT_SHOW, onShow, this);
// 兜底:某些环境(桌面浏览器)可能不会触发隐藏/显示事件,避免一直卡住
const FALLBACK_MS = 15000; // 按需调整
setTimeout(finish, FALLBACK_MS);
});
}
}
export default PaymentService;
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4474d3e2-964e-49b9-9793-7edab2f9d007",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,57 @@
/**
* 本地持久化未决订单,断线/重启后可恢复
*
*/
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(); // 返回加载的数据
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5165ac21-6ad4-444b-8575-47a851305a31",
"files": [],
"subMetas": {},
"userData": {}
}
+51
View File
@@ -0,0 +1,51 @@
/**
* 支付相关的类型定义
*
*/
export type PaymentChannel = "alipay" | "wxpay" | "stripe" | "paypal" | "url";
export type OrderStatus =
| "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 {
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;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5d3e814f-7ee9-463f-a568-c5f721f73498",
"files": [],
"subMetas": {},
"userData": {}
}
File diff suppressed because it is too large Load Diff