import cloud from "@lafjs/cloud"; import { createHmac, timingSafeEqual } from "crypto"; import { TABLES as T, reject, failure, key, open, validatePeriodId, identifier, isProduct } from "@/goldMiner/config"; import { resolvePeriodDates } from "@/goldMiner/testPeriods"; import { collection, authenticate, getUser, currentPeriod, ensurePlayer, playerId, ensureEntitlement, activateEntitlement, reserveEntitlement, insertOnce, mutatePlayer, closePlayer } from "@/goldMiner/service"; const orders = () => collection("order"); /** Both channels reserve the same account/period order before contacting a provider. */ export async function prepareOrder(body: any, user: any, platform: "and" | "ios", channel: "native" | "legacy_ios", channelSnapshot: any = {}) { const env = process.env.PAYMENT_APP_ENV || "production"; if ((!isProduct(body.itemid) && env !== 'test') || !identifier(body.createRequestId)) reject("INVALID_INPUT"); if (!["production", "test"].includes(env)) reject("PAYMENT_CONFIG_UNAVAILABLE", "PAYMENT_APP_ENV 必须为 production 或 test"); // Repair confirmed prior orders before exposing a purchase in their carry target period. const previous = await orders().find({ "goldMiner.uid": body.uid, "goldMiner.fulfillmentStatus": { $in: ["pending", "processing", "retryable"] }, "goldMiner.confirmedAt": { $gt: 0 } }).limit(100).toArray(); for (const order of previous) await fulfillOrder(order.outTradeNo); const period = await currentPeriod(Date.now(), body.uid); if (!period || !open(period)) reject("PERIOD_ENDED"); if (!period.purchaseEnabled) reject("PURCHASE_DISABLED"); const row = await ensurePlayer(body.uid, user, period); await ensureEntitlement(body.uid, period.periodId); const fresh = await mutatePlayer(row._id, state => { if (state.entitlementStatus === "unlocked" || state.entitlement) reject("ALREADY_UNLOCKED"); if (state.configSnapshot.productId !== body.itemid) reject("INVALID_PRODUCT", "商品与当前 VIP 礼包不匹配,请刷新活动信息后重试"); return state.offerLockedAt ? null : { offerLockedAt: Date.now() }; }, true); const offer = fresh.configSnapshot; // One stable order per account/period prevents concurrent clients creating two charges. const id = key("goldMinerOrder", env, body.uid, period.periodId); const outTradeNo = (env === "test" ? "wct_" : "wcx_") + id.slice(0, 24); let order = await insertOnce("order", { _id: id, openid: user.openid, outTradeNo, paymentAppEnv: env, itemid: body.itemid, itemCount: 1, goodsPrice: offer.priceFen, time: Date.now(), orderTime: new Date(Date.now() + 8 * 3600000), chargeTime: 0, getTime: 0, type: platform, state: 0, goldMiner: { schemaVersion: 1, uid: body.uid, accountScope: "users", paymentChannel: channel, ...channelSnapshot, periodId: period.periodId, startsAt: period.startsAt, endsAt: period.endsAt, configVersion: fresh.configVersion, configHash: fresh.configHash, ...(offer.vipLevel !== undefined ? { vipLevel: offer.vipLevel } : {}), productSnapshot: { productId: body.itemid, priceFen: offer.priceFen }, createRequestId: body.createRequestId, confirmedAt: 0, fulfillmentStatus: "pending", fulfillmentRoute: null, targetPeriodId: null, revision: 0 } }); if ((order.goldMiner.paymentChannel || "native") !== channel) reject("PAYMENT_CHANNEL_CONFLICT", "本期已有其他渠道订单,请继续原渠道支付"); if (order.goldMiner.confirmedAt || order.state !== 0) { await fulfillOrder(outTradeNo); reject("ALREADY_UNLOCKED"); } // Recheck an entitlement inserted while the order was being created. if ((await collection(T.players).findOne({ _id: row._id }))?.entitlement) reject("ALREADY_UNLOCKED"); if (!open(period)) reject("PERIOD_ENDED"); return order; } export async function createNativeOrder(body: any, platform: "and" | "ios") { try { const user = await authenticate(body); const env = process.env.PAYMENT_APP_ENV || "production"; if ((!isProduct(body.itemid) && env !== 'test') || !identifier(body.createRequestId)) reject("INVALID_INPUT"); const signKey = process.env.WX_MIDAS_PAY_SIGN_KEY; if (!["production", "test"].includes(env)) reject("PAYMENT_CONFIG_UNAVAILABLE", "PAYMENT_APP_ENV 必须为 production 或 test"); if (!signKey) reject("PAYMENT_CONFIG_UNAVAILABLE", "服务端缺少 WX_MIDAS_PAY_SIGN_KEY 配置"); if (!user.session_key) reject("PAYMENT_CONFIG_UNAVAILABLE", "玩家缺少有效 session_key,请重新登录后再下单"); const order = await prepareOrder(body, user, platform, "native"); const outTradeNo = order.outTradeNo; const signData = JSON.stringify({ mode: "goods", offerId: "1450527458", buyQuantity: 1, env: 0, currencyType: "CNY", platform: "android", zoneId: "1", productId: order.itemid, goodsPrice: order.goodsPrice, outTradeNo, attach: "黄金矿工" }); return { code: 1, data: { outTradeNo, signData, signature: createHmac("sha256", user.session_key).update(signData).digest("hex"), paySig: createHmac("sha256", signKey).update("requestMidasPaymentGameItem&" + signData).digest("hex") }, msg: "成功" }; } catch (error) { return failure(error); } } export function verifyNotification(body: any, payload: any, order: any) { const secret = process.env.WX_MIDAS_PAY_SIGN_KEY; const signature = body?.MiniGame?.PayEventSig; const env = process.env.PAYMENT_APP_ENV || "production"; if (!secret || body?.Event !== "minigame_game_pay_goods_deliver_notify" || body?.MiniGame?.IsMock === true || typeof body?.MiniGame?.Payload !== "string" || payload.Env !== 0 || !["production", "test"].includes(env) || order.paymentAppEnv !== env || typeof signature !== "string" || !/^[a-fA-F0-9]{64}$/.test(signature) || !order.goldMiner || (order.goldMiner.paymentChannel || "native") !== "native" || order.goldMiner.schemaVersion !== 1 || payload.OutTradeNo !== order.outTradeNo || payload.OpenId !== order.openid || payload.GoodsInfo?.ProductId !== order.itemid || Number(payload.GoodsInfo?.Quantity) !== Number(order.itemCount) || Number(payload.GoodsInfo?.OrigPrice) !== Number(order.goodsPrice)) reject("INVALID_PAYMENT"); validatePeriodId(order.goldMiner.periodId); const expected = createHmac("sha256", secret).update(body.Event + "&" + body.MiniGame.Payload).digest(); if (!timingSafeEqual(expected, Buffer.from(signature, "hex"))) reject("INVALID_PAYMENT"); } export async function confirmNativePayment(body: any, payload: any, order: any) { verifyNotification(body, payload, order); const user = await getUser(order.goldMiner.uid); if (!user || user.openid !== order.openid) reject("INVALID_PAYMENT"); await orders().updateOne({ outTradeNo: order.outTradeNo, "goldMiner.confirmedAt": 0 }, { $set: { state: 1, chargeTime: new Date(Date.now() + 8 * 3600000), "goldMiner.confirmedAt": Date.now() } }); return fulfillOrder(order.outTradeNo); } export async function fulfillOrder(outTradeNo: string) { let order = await orders().findOne({ outTradeNo }); if (!order?.goldMiner || !order.goldMiner.confirmedAt || ![1, 2].includes(order.state)) reject("PAYMENT_PENDING"); validatePeriodId(order.goldMiner.periodId); if (order.goldMiner.fulfillmentStatus === "fulfilled") return order.goldMiner; if (order.goldMiner.fulfillmentStatus === "manual_review") reject("MANUAL_REVIEW"); try { const g = order.goldMiner; const originalId = playerId(g.uid, g.periodId); let original = await collection(T.players).findOne({ _id: originalId }); if (!original) reject("NOT_FOUND", "缺少订单对应的活动快照"); if (!g.fulfillmentRoute) { let route = "current_period"; if (Date.now() >= g.endsAt) { original = await closePlayer(originalId); route = original.tasks.some((t: any) => t.targetWins <= original.progressAtClose && t.claimStatus !== "claimed") ? "settle_original" : "carry_next"; } const targetPeriodId = route === "carry_next" ? (await resolvePeriodDates(g.periodId)).nextPeriodId : g.periodId; await orders().updateOne({ outTradeNo, "goldMiner.fulfillmentRoute": null }, { $set: { "goldMiner.fulfillmentRoute": route, "goldMiner.targetPeriodId": targetPeriodId, "goldMiner.fulfillmentStatus": "processing" } }); order = await orders().findOne({ outTradeNo }); } const current = order.goldMiner; const dates = await resolvePeriodDates(current.targetPeriodId); const eid = playerId(current.uid, current.targetPeriodId); let entitlement = (await collection(T.players).findOne({ _id: eid }))?.entitlement; if (!entitlement && current.fulfillmentRoute === "carry_next" && Date.now() >= dates.endsAt) reject("ENTITLEMENT_CONFLICT", "顺延目标期已结束,需要人工核对"); if (!entitlement && current.fulfillmentRoute === "carry_next") { const pendingPurchase = await orders().findOne({ "goldMiner.uid": current.uid, "goldMiner.periodId": current.targetPeriodId }); if (pendingPurchase) reject("ENTITLEMENT_CONFLICT", "顺延期已有购买订单,需要核对后分配资格"); } entitlement = entitlement || await reserveEntitlement({ _id: eid, uid: current.uid, accountScope: "users", sourceOrderNo: outTradeNo, sourcePeriodId: current.periodId, targetPeriodId: current.targetPeriodId, source: current.fulfillmentRoute === "carry_next" ? "delayed_payment_carry" : current.fulfillmentRoute === "settle_original" ? "delayed_payment_original" : "purchase", state: "reserved", carryCount: current.fulfillmentRoute === "carry_next" ? 1 : 0, ...(original.configSnapshot?.vipLevel !== undefined ? { configSnapshot: original.configSnapshot } : {}), createdAt: Date.now(), activatesAt: dates.startsAt }); if (entitlement.sourceOrderNo !== outTradeNo) reject("ENTITLEMENT_CONFLICT"); if (current.fulfillmentRoute === "carry_next") await mutatePlayer(originalId, row => { if (row.carryTargetPeriodId === current.targetPeriodId) return null; return { carryTargetPeriodId: current.targetPeriodId, carryDisposition: "reserved" }; }); await activateEntitlement(entitlement); await cloud.database().collection("users").where({ _id: current.uid }).update({ pay_user: true }); await orders().updateOne({ outTradeNo }, { $set: { state: 2, getTime: new Date(Date.now() + 8 * 3600000), "goldMiner.fulfillmentStatus": "fulfilled", "goldMiner.entitlementId": eid, "goldMiner.fulfilledAt": Date.now() } }); return (await orders().findOne({ outTradeNo })).goldMiner; } catch (error: any) { const manual = error.errorCode === "ENTITLEMENT_CONFLICT"; await orders().updateOne({ outTradeNo, "goldMiner.fulfillmentStatus": { $ne: "fulfilled" } }, { $set: { "goldMiner.fulfillmentStatus": manual ? "manual_review" : "retryable", "goldMiner.lastErrorCode": error.errorCode || "RETRYABLE", "goldMiner.nextRetryAt": Date.now() + 60000 } }); throw error; } } export async function paymentInfo(body: any, order: any) { try { const user = await authenticate(body); if (order.goldMiner?.uid !== body.uid || order.openid !== user.openid) reject("UNAUTHORIZED"); if (!order.goldMiner.confirmedAt) reject("PAYMENT_PENDING"); const result = await fulfillOrder(order.outTradeNo); return { code: 1, data: { pay_state: 2, goldMiner: result, rewardDelivery: "goldMiner.claim" }, msg: "活动权益已确认" }; } catch (error) { return failure(error); } } export default async function () { return { code: 0, msg: "internal module" }; }