import cloud from '@lafjs/cloud'; import Utils from '@/Utils'; import { PRODUCT, type CoinState, type CoinConfig, parse, validConfig, createState, active, win, reward } from '@/coinMadnessModel'; const db = cloud.database(); const FIELD = 'coinMadnessState'; const PENDING = 'coinMadnessPending'; const changed = (result: any) => result?.ok && result.updated === 1; const failure = (msg: string) => ({ code: 0, data: null, msg }); function configuration(): CoinConfig | null { const raw = parse(process.env.COIN_MADNESS_CONFIG); return validConfig(raw) ? raw : null; } async function userById(uid: string) { return (await db.collection('users').where({ _id: uid }).getOne()).data; } function authenticate(user: any, token: any) { if (!user || !user.token || !Utils.checkToken(token, user.token)) throw new Error('token校验失败'); } async function save(user: any, state: CoinState, extra: any = {}) { // Missing fields must use exists(false), rather than undefined (which DB drivers may drop). const old = user[FIELD] === undefined ? db.command.exists(false) : user[FIELD]; return changed(await db.collection('users').where({ _id: user._id, [FIELD]: old }) .update({ [FIELD]: JSON.stringify(state), ...extra })); } function view(user: any, state: CoinState | null, now: number) { return { code: 1, data: { available: !!state, serverNow: now, state, pending: parse(user[PENDING]), needsSettlement: !!state && now >= state.config.endsAt && !state.settled } }; } export default async function coinMadness(ctx: FunctionContext) { const body = ctx.body || {}; try { const user = await userById(body.uid); authenticate(user, body.token); if (body.gameName === 'iaa') return view({}, null, Date.now()); const now = Date.now(); let state = parse(user[FIELD]); const pending: any = parse(user[PENDING]); if (body.action === 'ack') { if (!pending) return view(user, state, now); if (pending.id !== body.receiptId) throw new Error('奖励确认编号不匹配'); const coinAmount = Number(body.coinAmount); if (!Number.isSafeInteger(coinAmount) || coinAmount < 0) throw new Error('金币数据无效'); const ret = await db.collection('users').where({ _id: user._id, [PENDING]: user[PENDING] }) .update({ [PENDING]: null, coinAmount, timestamp: now, coinMadnessWalletRevision: Number(user.coinMadnessWalletRevision || 0) + 1 }); if (!changed(ret)) throw new Error('奖励状态已变化,请刷新'); return view({ ...user, [PENDING]: null }, state, now); } if (!['read', 'claim', 'settle'].includes(body.action)) throw new Error('无效的活动操作'); if (pending) return view(user, state, now); // Replay one durable receipt until acknowledged. if (body.action === 'read') { const config = configuration(); const eligible = config && now >= config.startsAt && now < config.endsAt && Number(user.levelAmount || 0) >= config.minLevel; if (eligible && (!state || (state.settled && state.config.periodId !== config.periodId))) { state = createState(config, Number(user.levelAmount) || 0); if (!await save(user, state)) throw new Error('活动状态已变化,请刷新'); } // Disabled configuration hides unpurchased offers; paid snapshots remain serviceable. if (state && !state.paidAt && (!eligible || state.config.periodId !== config.periodId)) { if (now < state.config.endsAt) return view(user, null, now); } return view(user, state, now); } if (!state || body.periodId !== state.config.periodId) throw new Error('活动期次已变化,请刷新'); if (body.action === 'settle' && state.settled) return view(user, state, now); if (body.action === 'claim' && Number(body.stage) !== state.stage) throw new Error('阶段已变化,请刷新'); const result = reward(state, now, body.action === 'settle'); result.state.lastLevel = Math.max(state.lastLevel, Number(user.levelAmount) || 0); const extra: any = {}; if (result.coins) { const base = Number(body.coinAmount); if (!Number.isSafeInteger(base) || base < 0 || !Number.isSafeInteger(base + result.coins)) throw new Error('金币数据无效'); extra[PENDING] = JSON.stringify({ id: state.config.periodId + ':' + state.stage, coins: result.coins, balance: base + result.coins, expired: body.action === 'settle' }); extra.coinAmount = base + result.coins; extra.coinMadnessWalletRevision = Number(user.coinMadnessWalletRevision || 0) + 1; extra.timestamp = now; } if (!await save(user, result.state, extra)) throw new Error('活动状态已变化,请刷新'); return view({ ...user, ...extra }, result.state, now); } catch (error) { return failure(error.message || '活动处理失败'); } } /** Called only by native order creation. One reusable order number per account/period. */ export async function prepareCoinMadnessOrder(user: any, body: any, candidate: string) { authenticate(user, body.token); const config = configuration(); const now = Date.now(); const state = parse(user[FIELD]); if (!config || !state || config.periodId !== state.config.periodId || !active(state, now) || state.paidAt || user[PENDING] || Number(user.levelAmount || 0) < config.minLevel) throw new Error('当前无法购买金币狂热'); const level = Number(body.coinMadnessLevel); if (!Number.isSafeInteger(level) || level < Number(user.levelAmount || 0) || level > Number(user.levelAmount || 0) + 10) throw new Error('请先同步主线关卡'); if (!state.reservedOrder || level > state.lastLevel) { if (!state.reservedOrder) state.reservedOrder = candidate; state.lastLevel = Math.max(state.lastLevel, level); if (!await save(user, state)) throw new Error('订单创建中,请重试'); } return { orderNo: state.reservedOrder, price: state.config.priceCents, snapshot: { coinMadnessPeriod: state.config.periodId } }; } export async function confirmCoinMadnessPayment(order: any, now = Date.now()) { // Retry CAS because victory reporting and payment notification can arrive together. for (let attempt = 0; attempt < 3; attempt++) { const user = (await db.collection('users').where({ openid: order.openid }).getOne()).data; if (!user) throw new Error('玩家不存在'); const state = parse(user[FIELD]); if (!state || state.config.periodId !== order.coinMadnessPeriod || state.reservedOrder !== order.outTradeNo) { await db.collection('order').where({ outTradeNo: order.outTradeNo }) .update({ coinMadnessReviewReason: 'period_mismatch', state: 1 }); throw new Error('Coin Madness 订单期次不匹配,需人工核对'); } if (state.orderNo === order.outTradeNo && state.paidAt) return; if (!active(state, now)) { await db.collection('order').where({ outTradeNo: order.outTradeNo }) .update({ coinMadnessReviewReason: 'confirmed_after_deadline', state: 1 }); throw new Error('付款确认晚于活动截止,需人工核对退款'); } state.paidAt = now; state.orderNo = order.outTradeNo; state.progress = 0; state.lastLevel = Math.max(state.lastLevel, Number(user.levelAmount) || 0); if (await save(user, state, { pay_user: true })) return; } throw new Error('付费解锁冲突,请重试通知'); } export async function recordCoinMadnessWin(uid: string, level: number, isVictory: boolean, infinite: boolean, now: number) { for (let attempt = 0; attempt < 3; attempt++) { const user = await userById(uid); const state = parse(user?.[FIELD]); if (!state || !state.paidAt || !active(state, now)) return; const next = win(state, level, now, isVictory, infinite); if (JSON.stringify(next) === JSON.stringify(state) || await save(user, next)) return; } throw new Error('Coin Madness 通关保存冲突'); } export { PRODUCT };