149 lines
10 KiB
TypeScript
149 lines
10 KiB
TypeScript
import cloud from '@lafjs/cloud';
|
|
import Utils from '@/Utils';
|
|
import { freezeUser, postSeasonOrderFields } from '@/passCheckSettlement';
|
|
const db = cloud.database();
|
|
const PERIOD = 30 * 24 * 3600000;
|
|
export const PASS_PRICES = { battlepass: 1800, battlepass_30: 3000, battlepass_12: 1200 };
|
|
export function parsePass(raw: any): any {
|
|
try { return typeof raw === 'string' ? JSON.parse(raw) || {} : raw || {}; } catch (_) { return {}; }
|
|
}
|
|
export function isUpgradeOrder(order: any): boolean {
|
|
return order?.passVersion === 2 || order?.itemid === 'battlepass_30' || order?.itemid === 'battlepass_12';
|
|
}
|
|
export function passOrderFields(user: any, body: any): any {
|
|
if (!Object.prototype.hasOwnProperty.call(PASS_PRICES, body.itemid)) return {};
|
|
if (Number(body.passVersion) !== 2) {
|
|
if (body.itemid === 'battlepass') return {};
|
|
throw new Error('请更新客户端后购买高级战令');
|
|
}
|
|
if (body.passOfferToken) return postSeasonOrderFields(user, body);
|
|
const stage = parsePass(user.passCheck)[2];
|
|
if (!stage || String(stage.time) !== String(body.passEnd) || Number(stage.time) <= Date.now()) throw new Error('本期已结束,请重新打开战令');
|
|
const extra = parsePass(user.passCheckUpgrade)[String(stage.time)] || {};
|
|
const tier = extra.tier === 30 ? 30 : stage.activate || extra.activate ? 18 : 0;
|
|
if (body.itemid === 'battlepass_12' ? tier !== 18 : tier !== 0) throw new Error('战令购买状态已变化,请重新打开');
|
|
return { passVersion: 2, passEnd: String(stage.time) };
|
|
}
|
|
/** 仅已使用新版战令的账号合并领取记录;旧周期、旧经验和旧响应格式不变。 */
|
|
export function legacyPassView(raw: any, extraRaw: any): any {
|
|
if (!raw || !extraRaw) return raw;
|
|
const pass = parsePass(raw), extra = parsePass(extraRaw);
|
|
for (const key of [1, 2]) {
|
|
const stage = pass[key], saved = extra[String(stage?.time)];
|
|
if (!stage || !saved) continue;
|
|
stage.activate = !!stage.activate || !!saved.activate;
|
|
for (const lane of ['free', 'passCheck']) for (let i = 0; i < Math.min(30, (saved[lane] || []).length); i++) {
|
|
if (saved[lane][i] === 0) (stage[lane] || (stage[lane] = []))[i] = 0;
|
|
}
|
|
}
|
|
return JSON.stringify(pass);
|
|
}
|
|
export default async function (ctx: FunctionContext) {
|
|
const body = ctx.body;
|
|
const users = db.collection(body.gameName === 'iaa' ? 'usersAd' : 'users');
|
|
let { data: user } = await users.where({ _id: body.uid }).getOne();
|
|
if (!user || (user.token && !Utils.checkToken(body.token, user.token))) return { code: 0, msg: 'token校验失败' };
|
|
user = await freezeUser(user, body.gameName === 'iaa' ? 'usersAd' : 'users');
|
|
const extra = parsePass(user.passCheckUpgrade), base = parsePass(user.passCheck);
|
|
const revision = Number(user.passUpgradeRevision) || 0;
|
|
const reply = (data = {}) => ({ code: 1, data: { extra, legacy: user.passCheck, revision, coinAmount: user.coinAmount || 0, ...data } });
|
|
if (body.action === 'read') return reply();
|
|
if (body.action === 'legacyOrder') {
|
|
const orders = await db.collection('order').where({ openid: user.openid, itemid: 'battlepass', state: 2 }).get();
|
|
const order = (orders.data || []).find(o => !o.passVersion && !o.passGranted && [base[1], base[2]].some(s =>
|
|
!s?.activate && !extra[String(s?.time)]?.activate && Number(s?.time) > Date.now() - PERIOD
|
|
&& Number(o.time) >= Number(s.time) - PERIOD && Number(o.time) < Number(s.time)));
|
|
return reply({ outTradeNo: order?.outTradeNo });
|
|
}
|
|
let order: any, end: string, bonus = false, targetTier = 0;
|
|
if (body.outTradeNo) {
|
|
order = (await db.collection('order').where({ outTradeNo: body.outTradeNo }).getOne()).data;
|
|
if (!order || order.openid !== user.openid || !Object.prototype.hasOwnProperty.call(PASS_PRICES, order.itemid)
|
|
|| ![1, 2].includes(order.state)) return { code: 0, msg: '战令订单尚未支付或不属于当前玩家' };
|
|
if (order.passPostSeason) return reply({ postSeason: true, passEnd: String(order.passEnd), snapshot: user.passSettlements?.[String(order.passEnd)], outTradeNo: order.outTradeNo });
|
|
if (order.passGranted) return reply({ alreadyGranted: true });
|
|
end = String(order.passEnd || [base[1], base[2]].find(s => Number(order.time) >= Number(s?.time) - PERIOD && Number(order.time) < Number(s?.time))?.time || '');
|
|
const stage = [base[1], base[2]].find(s => String(s?.time) === end);
|
|
const saved = extra[end] || {};
|
|
if ((!stage && !saved.time) || Number(end) <= Date.now() - PERIOD) return { code: 410, msg: '订单所属期已过期,请联系客服处理' };
|
|
if ((saved.orders || []).includes(order.outTradeNo) || (!isUpgradeOrder(order) && order.state === 2 && stage?.activate)) {
|
|
await db.collection('order').where({ outTradeNo: order.outTradeNo }).update({ state: 2, passGranted: true });
|
|
return reply({ alreadyGranted: true });
|
|
}
|
|
const owned = saved.tier === 30 ? 30 : saved.activate || stage?.activate ? 18 : 0;
|
|
targetTier = order.itemid === 'battlepass' ? 18 : 30;
|
|
if (owned >= targetTier || (order.itemid === 'battlepass_12' && owned !== 18)) return { code: 410, msg: '重复或无效升级订单,请联系客服处理' };
|
|
bonus = targetTier === 30 && !saved.bonusGranted;
|
|
if (body.action === 'order') return reply({ passEnd: end, targetTier, bonus });
|
|
}
|
|
if (body.action !== 'save') return { code: 0, msg: '无效战令操作' };
|
|
if (Number(body.revision) !== revision) return { code: 409, msg: '战令已更新,请重试' };
|
|
const supplied = parsePass(body.passCheck), legacy = parsePass(body.legacy);
|
|
const next: any = { ...extra };
|
|
let cutoff = Infinity;
|
|
for (const key of [1, 2]) {
|
|
let stage = supplied[key];
|
|
if (!stage || !Number(stage.time)) continue;
|
|
if (Number(stage.time) <= Date.now()) {
|
|
if (!order || String(stage.time) !== end) {
|
|
if (order) continue;
|
|
return { code: 410, msg: '上期进度已冻结,请通过结算领取' };
|
|
}
|
|
// A paid in-season order may arrive late. Only its original entitlement is recoverable;
|
|
// never accept a replacement expired progress/claim payload from the client.
|
|
stage = { ...user.passSettlements[end].stage };
|
|
if (bonus) {
|
|
// Legacy records encode XP as level + progress; do not drop that
|
|
// verified progress when adding the existing paid 20-XP entitlement.
|
|
const costs = [1, ...Array(8).fill(2), ...Array(21).fill(4)];
|
|
const level = Math.max(1, Math.min(30, Number(stage.progressLevel) || 1));
|
|
const legacyXP = costs.slice(0, level - 1).reduce((a, b) => a + b, 0) + Math.min(costs[level - 1], Number(stage.progress) || 0);
|
|
stage.experience = (Number.isFinite(stage.experience) ? stage.experience : legacyXP) + 20;
|
|
}
|
|
} else cutoff = Math.min(cutoff, Number(stage.time));
|
|
const time = String(stage.time), saved = extra[time] || {};
|
|
const old = [base[1], base[2]].find(s => String(s?.time) === time);
|
|
next[time] = { ...stage, experience: Math.max(saved.experience || 0, stage.experience || 0),
|
|
tier: saved.tier || 0, activate: !!saved.activate || !!old?.activate,
|
|
orders: saved.orders || [], bonusGranted: !!saved.bonusGranted };
|
|
for (const lane of ['free', 'passCheck']) {
|
|
next[time][lane] = [...(stage[lane] || [])];
|
|
for (let i = 0; i < next[time][lane].length; i++) {
|
|
if (saved[lane]?.[i] === 0 || (i < 30 && old?.[lane]?.[i] === 0)) next[time][lane][i] = 0;
|
|
}
|
|
}
|
|
if (legacy[key]) {
|
|
legacy[key].activate = next[time].activate;
|
|
legacy[key].free = next[time].free.slice(0, 30);
|
|
legacy[key].passCheck = next[time].passCheck.slice(0, 30);
|
|
}
|
|
}
|
|
if (order) {
|
|
if (!next[end]) return { code: 0, msg: '订单所属期数据缺失' };
|
|
next[end].tier = targetTier; next[end].activate = true;
|
|
next[end].bonusGranted = !!next[end].bonusGranted || bonus;
|
|
next[end].orders = [...next[end].orders, order.outTradeNo];
|
|
for (const key of [1, 2]) if (String(legacy[key]?.time) === end) legacy[key].activate = true;
|
|
}
|
|
const update: any = { passCheck: JSON.stringify(legacy), passCheckUpgrade: JSON.stringify(next), passUpgradeRevision: revision + 1 };
|
|
if (order && Number(end) <= Date.now() && user.passSettlements?.[end]) {
|
|
const frozen = user.passSettlements[end];
|
|
const recovered = { ...next[end] };
|
|
update.passSettlements = { ...user.passSettlements, [end]: { ...frozen, stage: recovered, claimed: false, emptySeen: false, offer: 'purchased' } };
|
|
update.passSettlementRevision = (Number(user.passSettlementRevision) || 0) + 1;
|
|
}
|
|
if (bonus) {
|
|
// 战令赠金由客户端计算,此处仅保存上传余额与同一期的发奖记录。
|
|
const coin = Number(body.coinAmount);
|
|
if (!Number.isSafeInteger(coin) || coin < 0) return { code: 0, msg: '战令奖励数据不完整' };
|
|
update.coinAmount = coin; update.timestamp = Date.now();
|
|
}
|
|
const where: any = { _id: user._id, passCheck: user.passCheck == null ? null : user.passCheck,
|
|
passUpgradeRevision: user.passUpgradeRevision == null ? null : user.passUpgradeRevision };
|
|
if (update.passSettlements) where.passSettlementRevision = user.passSettlementRevision ?? null;
|
|
if (Number.isFinite(cutoff)) where.$expr = { $lt: ['$$NOW', new Date(cutoff)] };
|
|
if ((await (cloud.mongo.db as any).collection(body.gameName === 'iaa' ? 'usersAd' : 'users').updateOne(where, { $set: update })).modifiedCount === 0) return { code: 409, msg: '战令已更新或到期,请重试' };
|
|
if (order) await db.collection('order').where({ outTradeNo: order.outTradeNo }).update({ state: 2, passGranted: true, getTime: new Date() });
|
|
return reply({ extra: next, legacy: update.passCheck, revision: revision + 1, bonus, targetTier, coinAmount: bonus ? update.coinAmount : user.coinAmount });
|
|
}
|