355 lines
11 KiB
TypeScript
355 lines
11 KiB
TypeScript
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 };
|
||
}
|