import cloud from "@lafjs/cloud"; import Utils from "@/Utils"; import * as JungleConfigModule from "@/jungleConfig"; // Laf 会为共享云函数生成 index 类型声明,该声明可能滞后于实际命名导出。 const JungleConfig: any = JungleConfigModule as any; const db = cloud.database(); const USER_STATE_FIELD = "jungleTreasureState"; const PENDING_SETTLEMENT_FIELD = "pendingJungleSettlement"; const VALID_ACTIONS = ["read", "claim", "settle_expired", "ack_settlement"]; /** * read:读取活动;claim:单档领取; * settle_expired:按客户端资源基数一次结算到期奖励;ack_settlement:确认结算弹窗。 */ export default async function jungleTreasure(ctx: FunctionContext) { const body = ctx.body || {}; const action = String(body.action || "").toLowerCase(); const uid = body.uid; if (!VALID_ACTIONS.includes(action)) { return fail("无效的 Jungle 操作类型"); } if (!uid) { return fail("未获取到uid"); } if (body.gameName === "iaa") { return noActivity("iaa_excluded"); } const userResult = await db.collection("users").where({ _id: uid }).getOne(); const user = userResult.data; if (!user) { return fail("未获取到玩家信息"); } if (user.token && !Utils.checkToken(body.token, user.token)) { return fail("token校验失败"); } const pendingSettlement = normalizePendingSettlement(user[PENDING_SETTLEMENT_FIELD]); if (action === "ack_settlement") { return acknowledgeSettlement(uid, body.settlementId, pendingSettlement); } if (pendingSettlement) { if (action === "settle_expired" && hasStoredValue(user[USER_STATE_FIELD])) { const pendingState = JungleConfig.normalizeStoredJungleState(user[USER_STATE_FIELD]); await markSettlementOrdersDelivered(user, pendingState, pendingSettlement.claimedTierIds || []); } return action === "read" || action === "settle_expired" ? settlementSuccess(pendingSettlement, "待确认的 Jungle 结算") : fail("请先确认已结算的 Jungle 奖励"); } const now = Date.now(); let storedStateValue: unknown = user[USER_STATE_FIELD]; if (action !== "read" && !hasStoredValue(storedStateValue)) { return fail("当前没有可处理的 Jungle 活动"); } const resolution = JungleConfig.resolveJungleActivity(storedStateValue, now, { // 只有 read 保持现有界面触发时机;领取与结算不负责开启新一期。 allowCalendarStart: action === "read", calendarUserId: uid, }); let serverState = resolution.state; if (serverState && resolution.changed) { storedStateValue = JungleConfig.serializeJungleState(serverState); const initialized = await db.collection("users").where({ _id: uid }).update({ [USER_STATE_FIELD]: storedStateValue, }); if (isUpdateFailed(initialized)) { return fail("礼包状态初始化失败"); } } if (!resolution.available || !serverState) { return action === "read" ? noActivity(resolution.reason) : fail("当前没有可处理的 Jungle 活动"); } if (action === "read") { return readSuccess(serverState, resolution.reason); } if (action === "settle_expired") { return settleExpiredRewards(uid, user, storedStateValue, serverState, body.resources, now); } const claimResult = JungleConfig.claimJungleTier(serverState, body.tierId, now); if (!claimResult.ok || !claimResult.state || !claimResult.tierId) { return fail(claimResult.message || "礼包领取失败"); } // 字符串不能按数组下标查询;比较完整旧值,避免并发请求覆盖彼此或重复领取。 const updateResult = await db.collection("users").where({ _id: uid, [USER_STATE_FIELD]: storedStateValue, }).update({ [USER_STATE_FIELD]: JungleConfig.serializeJungleState(claimResult.state), }); if (isUpdateFailed(updateResult)) { return fail("礼包状态已变化,请重新读取后再领取"); } if (claimResult.paidOrderNo) { await markOrderDelivered(user.openid, claimResult.paidOrderNo); } const latestUserResult = await db.collection("users").where({ _id: uid }).getOne(); if (!latestUserResult.data) { return fail("礼包领取后读取状态失败"); } serverState = JungleConfig.normalizeStoredJungleState(latestUserResult.data[USER_STATE_FIELD]); return claimSuccess(claimResult.tierId, serverState); } async function settleExpiredRewards( uid: string, user: any, storedStateValue: unknown, serverState: any, rawResources: unknown, now: number, ) { const resources = normalizeResourceSnapshot(rawResources); if (!resources) { return fail("Jungle 结算资源数据不合法"); } const settlement = JungleConfig.settleExpiredJungleTiers(serverState, now); if (!settlement.ok || !settlement.state || settlement.claimedTierIds.length === 0) { return fail(settlement.message || "当前没有可结算的 Jungle 奖励"); } const rewards = settlement.rewards; const serverNowSeconds = Math.floor(now / 1000); const balances = { coinAmount: resources.coinAmount + rewards.coin, freezeAmount: resources.freezeAmount + rewards.freeze, hammerAmount: resources.hammerAmount + rewards.hammer, magicAmount: resources.magicAmount + rewards.magic_wand, userPowerTime: rewards.infinite_health > 0 ? Math.max(resources.userPowerTime, serverNowSeconds) + rewards.infinite_health : resources.userPowerTime, }; if (!Object.values(balances).every((value) => Number.isSafeInteger(value) && value >= 0)) { return fail("Jungle 结算后的资源数据不合法"); } const pendingSettlement = { settlementId: createSettlementId(serverState), activityId: serverState.activityId, activityType: serverState.activityType, periodKey: serverState.periodKey, startsAt: serverState.startsAt, expiresAt: serverState.expiresAt, claimedTierIds: settlement.claimedTierIds, rewards, balances, createdAt: now, }; const updateResult = await db.collection("users").where({ _id: uid, [USER_STATE_FIELD]: storedStateValue, }).update({ [USER_STATE_FIELD]: JungleConfig.serializeJungleState(settlement.state), [PENDING_SETTLEMENT_FIELD]: pendingSettlement, ...balances, timestamp: now, }); if (isUpdateFailed(updateResult)) { const latest = await db.collection("users").where({ _id: uid }).getOne(); const existing = normalizePendingSettlement(latest.data?.[PENDING_SETTLEMENT_FIELD]); return existing ? settlementSuccess(existing, "Jungle 奖励已经结算") : fail("Jungle 状态已变化,请重新读取后再结算"); } await markSettlementOrdersDelivered(user, settlement.state, settlement.claimedTierIds); return settlementSuccess(pendingSettlement, "Jungle 奖励结算成功"); } async function acknowledgeSettlement(uid: string, rawSettlementId: unknown, pendingSettlement: any) { const settlementId = String(rawSettlementId || "").trim(); if (!settlementId) { return fail("未获取到 Jungle 结算编号"); } // 确认响应丢失后重试也返回成功;有其他待确认结算时则禁止误清理。 if (!pendingSettlement) { return ackSuccess(settlementId); } if (pendingSettlement.settlementId !== settlementId) { return fail("Jungle 结算编号不匹配"); } const updateResult = await db.collection("users").where({ _id: uid, [`${PENDING_SETTLEMENT_FIELD}.settlementId`]: settlementId, }).update({ [PENDING_SETTLEMENT_FIELD]: null, }); if (isUpdateFailed(updateResult)) { const latest = await db.collection("users").where({ _id: uid }).getOne(); const latestPending = normalizePendingSettlement(latest.data?.[PENDING_SETTLEMENT_FIELD]); if (latestPending && latestPending.settlementId !== settlementId) { return fail("Jungle 待确认结算已经变化"); } } return ackSuccess(settlementId); } async function markSettlementOrdersDelivered(user: any, state: any, tierIds: number[]) { for (const tierId of tierIds) { const orderNo = String(state.paidOrderNos?.[String(tierId)] || "").trim(); if (orderNo) { await markOrderDelivered(user.openid, orderNo); } } } async function markOrderDelivered(openid: unknown, orderNo: string) { await db.collection("order").where({ outTradeNo: orderNo, openid, state: 1, }).update({ state: 2, getTime: new Date(Date.now() + 8 * 3600000), }); } function readSuccess(state: any, reason: string) { const serverNow = Date.now(); const expiresAt = state.expiresAt; return { code: 1, data: { tierStates: state.tierStates, available: true, reason, activityType: state.activityType, periodKey: state.periodKey, startsAt: state.startsAt, revision: state.revision, expiresAt, serverNow, updatedAt: state.updatedAt, config: { ...JungleConfig.JUNGLE_TREASURE_CONFIG, endAt: expiresAt, }, }, msg: "读取成功", }; } function claimSuccess(claimedTierId: number, state: any) { return { code: 1, data: { claimedTierId, tierStates: state.tierStates, available: true, activityType: state.activityType, periodKey: state.periodKey, startsAt: state.startsAt, revision: state.revision, expiresAt: state.expiresAt, serverNow: Date.now(), updatedAt: state.updatedAt, }, msg: "领取成功", }; } function settlementSuccess(pendingSettlement: any, msg: string) { return { code: 1, data: { available: false, reason: "settlement_pending", pendingSettlement, serverNow: Date.now(), }, msg, }; } function ackSuccess(settlementId: string) { return { code: 1, data: { settlementId, acknowledged: true, serverNow: Date.now() }, msg: "Jungle 结算确认成功", }; } function noActivity(reason: string) { return { code: 1, data: { available: false, reason, serverNow: Date.now() }, msg: "当前没有可参加的 Jungle 活动", }; } function normalizeResourceSnapshot(raw: any) { if (typeof raw === "string") { try { raw = JSON.parse(raw); } catch { return null; } } if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return null; } const keys = ["coinAmount", "freezeAmount", "hammerAmount", "magicAmount", "userPowerTime"]; const values: any = {}; for (const key of keys) { const value = Number(raw[key]); if (!Number.isSafeInteger(value) || value < 0) { return null; } values[key] = value; } return values; } function normalizePendingSettlement(raw: any) { if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return null; } const settlementId = String(raw.settlementId || "").trim(); if (!settlementId || !raw.rewards || !normalizeResourceSnapshot(raw.balances)) { return null; } return raw; } function createSettlementId(state: any): string { return `${state.activityId}:${state.startsAt}:${state.expiresAt}`; } function hasStoredValue(value: unknown): boolean { return value !== undefined && value !== null && !(typeof value === "string" && !value.trim()); } function isUpdateFailed(result: any): boolean { return Boolean(result && (result.ok === false || result.updated === 0)); } function fail(msg: string) { return { code: 0, data: null, msg }; }