server/laf-cloud/functions/wucaiMigration.ts
guanchao e2a5c3d2f6 Squash local into main while preserving battle pass compatibility
Integrate starter pack timing, reactivation, purchase analytics, payment routing and atomic user IDs. Resolve iOS conflicts by preserving both battle pass delivery and starter pack snapshots. Add coexistence regression checks and record remaining compatibility risks. Validation: 118/125 tests passed; 7 existing failures remain, plus a separately reproduced pre-existing V2 task login failure.
2026-09-10 16:31:14 +08:00

216 lines
6.6 KiB
TypeScript

export const WUCAI_TYPE_NONE = -1;
export const WUCAI_TYPE_AUTO_MIGRATED = 0;
export const WUCAI_TYPE_PENDING_CHOICE = 1;
export const WUCAI_TYPE_CHOSE_WUCAI = 2;
export const WUCAI_TYPE_CHOSE_CURRENT = 3;
export const WUCAI_REWARD = {
coinAmount: 2000,
freezeAmount: 3,
hammerAmount: 3,
magicAmount: 3,
};
export default async function main() {
return { code: 1, data: null, msg: "wucaiMigration ready" };
}
const DEFAULT_HEAD_ARR = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
const DEFAULT_CAT_ARR = [1, 2, 3];
export async function fetchWucaiUser(unionid: string): Promise<any | null> {
if (!unionid) {
return null;
}
try {
const response = await fetch("https://laf.nika4games.com/getUserInfoByUnionid", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ unionid }),
});
if (response.ok === false) {
return null;
}
const result = await response.json();
const user = result?.data;
if (!user || typeof user !== "object"
|| !Object.prototype.hasOwnProperty.call(user, "levelAmount")) {
return null;
}
return user;
} catch (error) {
console.error("Failed to read Wucai user", error);
return null;
}
}
export function normalizeWucaiUser(source: any): any {
return {
levelAmount: numberOr(source?.levelAmount, 0),
coinAmount: numberOr(source?.coinAmount, 0),
healthAmount: numberOr(source?.healthAmount, 5),
healthtimestamp: numberOr(source?.healthtimestamp, 0),
freezeAmount: numberOr(source?.freezeAmount, 3),
hammerAmount: numberOr(source?.hammerAmount, 3),
magicAmount: numberOr(source?.magicAmount, 3),
userPowerTime: numberOr(source?.userPowerTime, 0),
monthCardReward: numberOr(source?.monthCardReward, 0),
monthCardTime: numberOr(source?.monthCardTime, 0),
pay_user: booleanOrFalse(source?.pay_user),
starter_pack: numberOr(source?.starter_pack, 0),
starter_packState: numberOr(source?.starter_packState, 0),
starterPackVersion: numberOr(source?.starterPackVersion, 1),
starterPackRound: numberOr(source?.starterPackRound, 0),
winStreak: numberOr(source?.winStreak, 0),
passCheck: cloneSerializable(source?.passCheck ?? null),
endLevelNum: numberOr(source?.endLevelNum, 0),
totalLevel: numberOr(source?.totalLevel, 0),
userLevel: numberOr(source?.userLevel, 0),
film: numberOr(source?.film, 0),
headArr: numberArrayOr(source?.headArr, DEFAULT_HEAD_ARR),
catArr: numberArrayOr(source?.catArr, DEFAULT_CAT_ARR),
};
}
export function buildWucaiPreview(source: any, nowSeconds = Math.floor(Date.now() / 1000)): any {
const user = normalizeWucaiUser(source);
return {
levelAmount: user.levelAmount,
coinAmount: user.coinAmount,
healthAmount: user.healthAmount,
freezeAmount: user.freezeAmount,
hammerAmount: user.hammerAmount,
magicAmount: user.magicAmount,
film: user.film,
winStreak: user.winStreak,
headCount: user.headArr.length,
catCount: user.catArr.length,
monthCardActive: user.monthCardTime > nowSeconds,
passCheckActive: isPassCheckActive(user.passCheck),
};
}
export function addWucaiReward(user: any): any {
return {
coinAmount: numberOr(user?.coinAmount, 0) + WUCAI_REWARD.coinAmount,
freezeAmount: numberOr(user?.freezeAmount, 0) + WUCAI_REWARD.freezeAmount,
hammerAmount: numberOr(user?.hammerAmount, 0) + WUCAI_REWARD.hammerAmount,
magicAmount: numberOr(user?.magicAmount, 0) + WUCAI_REWARD.magicAmount,
};
}
export function isPristineWucaiTarget(user: any, hasOrder: boolean): boolean {
if (!user || hasOrder) {
return false;
}
const expectedNumbers: Record<string, number> = {
levelAmount: 0,
coinAmount: 0,
healthAmount: 5,
healthtimestamp: 0,
freezeAmount: 3,
hammerAmount: 3,
magicAmount: 3,
userPowerTime: 0,
monthCardReward: 0,
monthCardTime: 0,
rebornGiftCount: 0,
rebornGiftTime: 0,
starter_pack: 0,
starter_packState: 0,
winStreak: 0,
endLevelNum: 0,
totalLevel: 0,
userLevel: 0,
film: 0,
careerAmount: 0,
addLevel: 0,
};
for (const key of Object.keys(expectedNumbers)) {
if (numberOr(user[key], expectedNumbers[key]) !== expectedNumbers[key]) {
return false;
}
}
if (booleanOrFalse(user.pay_user)) {
return false;
}
if (user.passCheck != null && user.passCheck !== "null" && user.passCheck !== "") {
return false;
}
if (hasMeaningfulJungleState(user.jungleTreasureState)) {
return false;
}
return arraysEqual(numberArrayOr(user.headArr, DEFAULT_HEAD_ARR), DEFAULT_HEAD_ARR)
&& arraysEqual(numberArrayOr(user.catArr, DEFAULT_CAT_ARR), DEFAULT_CAT_ARR);
}
export function isValidWucaiType(value: any): boolean {
const type = Number(value);
return Number.isInteger(type) && type >= WUCAI_TYPE_NONE && type <= WUCAI_TYPE_CHOSE_CURRENT;
}
function numberOr(value: any, fallback: number): number {
if (value === null || value === undefined || value === "") {
return fallback;
}
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
function booleanOrFalse(value: any): boolean {
return value === true || value === 1 || value === "1" || value === "true";
}
function numberArrayOr(value: any, fallback: number[]): number[] {
if (!Array.isArray(value)) {
return [...fallback];
}
return value.map(item => Number(item)).filter(item => Number.isFinite(item));
}
function cloneSerializable(value: any): any {
if (value == null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
return value;
}
return JSON.parse(JSON.stringify(value));
}
function isPassCheckActive(value: any): boolean {
let passCheck = value;
if (typeof passCheck === "string") {
try {
passCheck = JSON.parse(passCheck);
} catch {
return false;
}
}
if (Array.isArray(passCheck)) {
return passCheck.some(item => item?.activate === true || item?.activate === 1);
}
return passCheck?.activate === true || passCheck?.activate === 1;
}
function hasMeaningfulJungleState(value: any): boolean {
if (value == null || value === "" || value === "null") {
return false;
}
let state = value;
if (typeof state === "string") {
try {
state = JSON.parse(state);
} catch {
return true;
}
}
if (!state || !Array.isArray(state.tierStates)) {
return true;
}
return state.tierStates.some(item => Number(item) > 0);
}
function arraysEqual(left: number[], right: number[]): boolean {
return left.length === right.length && left.every((item, index) => item === right[index]);
}