server/laf-cloud/functions/goldMiner/jobs.ts
2026-09-24 17:59:52 +08:00

60 lines
4.4 KiB
TypeScript

import * as ConfigStore from "@/activityConfig/store";
import { reconcileLegacyOrder } from "@/goldMiner/legacyPayment";
import { TABLES as T } from "@/goldMiner/config";
import { collection, currentPeriod, closePlayer, activateEntitlement } from "@/goldMiner/service";
import { fulfillOrder } from "@/goldMiner/payment";
/** Bounded pending-state batches: successful rows leave the queue; failures back off. */
export default async function () {
const result: any = { players: 0, entitlements: 0, orders: 0, merchant: 0, failures: 0 };
try { await currentPeriod(); }
catch { result.failures++; console.error("goldMiner current configuration requires review"); }
const due = (field: string) => ({ $or: [{ [field]: { $exists: false } }, { [field]: { $lte: Date.now() } }] });
const scans = [
{ name: "merchant", table: "order", batch: 5, retry: "goldMiner.nextQueryAt", filter: { "goldMiner.paymentChannel": "legacy_ios", "goldMiner.confirmedAt": 0,
"goldMiner.prepayState": { $nin: ["closed", "paid"] }, "goldMiner.nextQueryAt": { $lte: Date.now() }, "goldMiner.ioLeaseUntil": { $lte: Date.now() } },
run: reconcileLegacyOrder },
{ name: "entitlements", table: T.players, retry: "entitlementNextRetryAt", filter: { "entitlement.state": "reserved", "entitlement.activatesAt": { $lte: Date.now() }, ...due("entitlementNextRetryAt") },
run: (r: any) => activateEntitlement(r.entitlement) },
{ name: "players", table: T.players, retry: "settlementNextRetryAt", filter: { endsAt: { $lte: Date.now() }, tasks: { $exists: true }, settlementPreparedAt: null, ...due("settlementNextRetryAt") },
run: (r: any) => closePlayer(r._id) },
{ name: "orders", table: "order", retry: "goldMiner.nextRetryAt", filter: { "goldMiner.confirmedAt": { $gt: 0 }, "goldMiner.fulfillmentStatus": { $in: ["pending", "processing", "retryable"] }, ...due("goldMiner.nextRetryAt") },
run: (r: any) => fulfillOrder(r.outTradeNo) },
];
for (const scan of scans) {
const rows = await collection(scan.table).find(scan.filter).sort({ [scan.retry]: 1, _id: 1 }).limit(scan.batch || 100).toArray();
for (const row of rows) {
try { await scan.run(row); result[scan.name]++; }
catch {
result.failures++;
// Keep a longer merchant/provider backoff already recorded by the handler.
const nextRetryAt = Date.now() + 60000;
try { await collection(scan.table).updateOne({ _id: row._id,
$or: [{ [scan.retry]: { $exists: false } }, { [scan.retry]: { $lt: nextRetryAt } }] },
{ $set: { [scan.retry]: nextRetryAt } }); }
catch { console.error("goldMiner retry scheduling failed", scan.name, String(row._id)); }
console.error("goldMiner job retry required", scan.name, String(row._id));
}
}
}
return result;
}
export async function setupIndexes() {
await ConfigStore.setupIndexes();
await collection(T.players).createIndex({ uid: 1, periodId: 1 }, { unique: true });
await collection(T.players).createIndex({ uid: 1, endsAt: 1, _id: 1 });
await collection(T.players).createIndex({ settlementPreparedAt: 1, settlementNextRetryAt: 1, endsAt: 1 });
await collection(T.players).createIndex({ "entitlement.sourceOrderNo": 1 }, { unique: true,
partialFilterExpression: { "entitlement.sourceOrderNo": { $type: "string" } } });
await collection(T.players).createIndex({ "entitlement.state": 1, "entitlement.activatesAt": 1, entitlementNextRetryAt: 1 });
await collection(T.players).createIndex({ settlementId: 1 }, { unique: true, partialFilterExpression: { settlementId: { $type: "string" } } });
await collection("order").createIndex({ outTradeNo: 1 }, { unique: true, partialFilterExpression: { "goldMiner.schemaVersion": 1 }, name: "goldMiner_order_no" });
await collection("order").createIndex({ "goldMiner.uid": 1, "goldMiner.periodId": 1 },
{ unique: true, partialFilterExpression: { "goldMiner.schemaVersion": 1 }, name: "goldMiner_player_period" });
await collection("order").createIndex({ "goldMiner.merchantSnapshot.mchid": 1, "goldMiner.transactionId": 1 },
{ unique: true, partialFilterExpression: { "goldMiner.paymentChannel": "legacy_ios", "goldMiner.transactionId": { $type: "string" } }, name: "goldMiner_merchant_transaction" });
await collection("order").createIndex({ "goldMiner.paymentChannel": 1, "goldMiner.confirmedAt": 1, "goldMiner.nextQueryAt": 1 });
await collection("order").createIndex({ "goldMiner.fulfillmentStatus": 1, "goldMiner.confirmedAt": 1, _id: 1 });
}