831 lines
31 KiB
TypeScript
831 lines
31 KiB
TypeScript
import cloud from '@lafjs/cloud'
|
||
import { createHash } from 'crypto';
|
||
import * as JungleConfigModule from "@/jungleConfig";
|
||
import {
|
||
WUCAI_TYPE_AUTO_MIGRATED,
|
||
WUCAI_TYPE_NONE,
|
||
WUCAI_TYPE_PENDING_CHOICE,
|
||
buildWucaiPreview,
|
||
fetchWucaiUser,
|
||
isPristineWucaiTarget,
|
||
isValidWucaiType,
|
||
normalizeWucaiUser,
|
||
} from "./wucaiMigration";
|
||
const JungleConfig: any = JungleConfigModule as any;
|
||
const db = cloud.database();
|
||
import crypto from 'crypto';
|
||
const mchid = process.env.WX_MCH_ID;//商户id
|
||
const serialNo = process.env.WX_MCH_CERT_SERIAL_NO;//证书序列号
|
||
const ThinkingData = require('thinkingdata-node');
|
||
import Ip2region from 'ip2region';
|
||
// 新版每日/周任务暂不上线,仅保留源码备查(743ea34 引入,local 分支保留)。
|
||
// import { refreshTaskData } from "@/dailyTaskService";
|
||
const query = new Ip2region();
|
||
const canIos = false;
|
||
const version = "9.9.99";
|
||
const AB_CONFIG_COLLECTION = "ab_config";
|
||
const MANAGED_AB_LAYERS = ["1", "2", "3"];
|
||
const DEFAULT_AB_GROUP = "Default";
|
||
export default async function (ctx: FunctionContext) {
|
||
const body = ctx.body || {};
|
||
let code = body.code;
|
||
let distinctId = body.distinctId;
|
||
let isDebug = body.isDebug;
|
||
const from = body.from;
|
||
const supportsWucaiMigration = Number(body.wucai_capability) >= 1;
|
||
let dbname = "users";
|
||
const gameName = body.gameName;
|
||
if (gameName == "iaa") {
|
||
dbname = "usersAd";
|
||
}
|
||
if (from === "wucai" && dbname !== "users") {
|
||
return wucaiLoginFail("WUCAI_UNSUPPORTED_GAME", "五彩账号迁移仅支持主用户库");
|
||
}
|
||
if (from === "wucai" && !supportsWucaiMigration) {
|
||
return wucaiLoginFail("WUCAI_CLIENT_UPGRADE_REQUIRED", "当前客户端不支持五彩账号迁移,请更新游戏");
|
||
}
|
||
|
||
let idname = process.env.IDCOUNT_NAME;
|
||
// if (gameName == "iaa") {
|
||
// idname = "69fabaacb4d89b170da4bf57";
|
||
// }
|
||
let info = await getWXOpenId(code, dbname);
|
||
if (!info) {
|
||
return;
|
||
}
|
||
let openid = info[0];
|
||
let session_key = info[1];
|
||
let unionid = info[2];
|
||
let userData = await getUserData(dbname, openid);
|
||
const isNewUser = !userData;
|
||
if (userData?.freeze) {
|
||
return { code: 0, data: "账号封禁", msg: "账号封禁" };
|
||
}
|
||
let token = await stringToHash(Date.now() + openid);
|
||
let abTests: any = null;
|
||
let shouldPersistABTests = false;
|
||
let wucaiPreview = null;
|
||
if (!userData) {
|
||
if (dbname === "users") {
|
||
abTests = await getActiveABAssignments(openid, null, true);
|
||
}
|
||
let migratedUser = null;
|
||
if (from === "wucai") {
|
||
const source = await fetchWucaiUser(unionid);
|
||
if (!source) {
|
||
return wucaiLoginFail("WUCAI_SOURCE_UNAVAILABLE", "五彩数据获取失败,请重新进入游戏后重试");
|
||
}
|
||
migratedUser = normalizeWucaiUser(source);
|
||
}
|
||
const onlyId = await allocateOnlyId(idname);
|
||
await db.collection(dbname).add({
|
||
openid: openid,
|
||
unionid: unionid,
|
||
session_key: session_key,
|
||
register_time: Date.now(),
|
||
onlyId: onlyId,
|
||
distinctId: distinctId,
|
||
isDebug: isDebug,
|
||
levelAmount: migratedUser?.levelAmount ?? 0,
|
||
coinAmount: migratedUser?.coinAmount ?? 0,
|
||
healthAmount: migratedUser?.healthAmount ?? 5,
|
||
healthtimestamp: migratedUser?.healthtimestamp ?? 0,
|
||
freezeAmount: migratedUser?.freezeAmount ?? 3,
|
||
hammerAmount: migratedUser?.hammerAmount ?? 3,
|
||
magicAmount: migratedUser?.magicAmount ?? 3,
|
||
userPowerTime: migratedUser?.userPowerTime ?? 0,
|
||
monthCardReward: migratedUser?.monthCardReward ?? 0,
|
||
monthCardTime: migratedUser?.monthCardTime ?? 0,
|
||
rebornGiftCount: 0,
|
||
rebornGiftTime: 0,
|
||
useravatar: Math.floor(Math.random() * 4) + "",
|
||
username: "user",
|
||
pay_user: migratedUser?.pay_user ?? false,
|
||
isWhite: false,
|
||
isFirst: true,
|
||
starter_pack: migratedUser?.starter_pack ?? 0,
|
||
starter_packState: migratedUser?.starter_packState ?? 0,
|
||
starterPackVersion: migratedUser?.starterPackVersion ?? 1,
|
||
starterPackRound: migratedUser?.starterPackRound ?? 0,
|
||
winStreak: migratedUser?.winStreak ?? 0,
|
||
passCheck: migratedUser?.passCheck ?? null,
|
||
endLevelNum: migratedUser?.endLevelNum ?? 0,
|
||
totalLevel: migratedUser?.totalLevel ?? 0,
|
||
userLevel: migratedUser?.userLevel ?? 0,
|
||
film: migratedUser?.film ?? 0,
|
||
careerAmount: 0,
|
||
useravatarIcon: "0",
|
||
userCat: (Math.floor(Math.random() * 3) + 1) + "",
|
||
forcedUpdate: !!migratedUser,
|
||
...(dbname === "users" ? {
|
||
wucai_type: migratedUser ? WUCAI_TYPE_AUTO_MIGRATED : WUCAI_TYPE_NONE,
|
||
} : {}),
|
||
headArr: migratedUser?.headArr ?? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||
catArr: migratedUser?.catArr ?? [1, 2, 3],
|
||
token: token,
|
||
...(dbname === "users" ? { abTests: abTests } : {})
|
||
});
|
||
} else {
|
||
const migrationUpdate: any = {};
|
||
if (dbname === "users") {
|
||
const hasStoredWucaiType = Object.prototype.hasOwnProperty.call(userData, "wucai_type");
|
||
if (hasStoredWucaiType && !isValidWucaiType(userData.wucai_type)) {
|
||
return wucaiLoginFail("WUCAI_INVALID_STATE", "五彩迁移状态异常,请联系客服");
|
||
}
|
||
|
||
let wucaiType = hasStoredWucaiType
|
||
? Number(userData.wucai_type)
|
||
: WUCAI_TYPE_NONE;
|
||
if (!hasStoredWucaiType) {
|
||
migrationUpdate.wucai_type = WUCAI_TYPE_NONE;
|
||
}
|
||
|
||
if ((wucaiType === WUCAI_TYPE_AUTO_MIGRATED || wucaiType === WUCAI_TYPE_PENDING_CHOICE)
|
||
&& !supportsWucaiMigration) {
|
||
return wucaiLoginFail("WUCAI_CLIENT_UPGRADE_REQUIRED", "当前账号需要更新游戏后继续五彩账号迁移");
|
||
}
|
||
|
||
let source = null;
|
||
if (wucaiType === WUCAI_TYPE_PENDING_CHOICE
|
||
|| (wucaiType === WUCAI_TYPE_NONE && from === "wucai")) {
|
||
source = await fetchWucaiUser(unionid || userData.unionid);
|
||
if (!source) {
|
||
return wucaiLoginFail("WUCAI_SOURCE_UNAVAILABLE", "五彩数据获取失败,请重新进入游戏后重试");
|
||
}
|
||
}
|
||
|
||
if (wucaiType === WUCAI_TYPE_NONE && from === "wucai") {
|
||
const hasOrder = await hasUserOrder(userData.openid);
|
||
if (isPristineWucaiTarget(userData, hasOrder)) {
|
||
Object.assign(migrationUpdate, normalizeWucaiUser(source), {
|
||
wucai_type: WUCAI_TYPE_AUTO_MIGRATED,
|
||
forcedUpdate: true,
|
||
});
|
||
wucaiType = WUCAI_TYPE_AUTO_MIGRATED;
|
||
} else {
|
||
migrationUpdate.wucai_type = WUCAI_TYPE_PENDING_CHOICE;
|
||
wucaiType = WUCAI_TYPE_PENDING_CHOICE;
|
||
wucaiPreview = buildWucaiPreview(source);
|
||
}
|
||
} else if (wucaiType === WUCAI_TYPE_PENDING_CHOICE) {
|
||
wucaiPreview = buildWucaiPreview(source);
|
||
}
|
||
}
|
||
|
||
if (dbname === "users") {
|
||
abTests = await getActiveABAssignments(openid, userData.abTests, false);
|
||
shouldPersistABTests = JSON.stringify(userData.abTests) !== JSON.stringify(abTests);
|
||
}
|
||
/* 新版每日/周任务暂不执行;启用时需替换下方旧版逻辑并配套客户端。
|
||
const task = refreshTaskData(
|
||
userData.task,
|
||
userData._id,
|
||
Number(userData.levelAmount) + 1 || 1,
|
||
).data;
|
||
*/
|
||
// 当前上线使用旧版四项任务逻辑:743ea34 之前的实现,8e4a978 已恢复。
|
||
let taskTime = userData.taskTime || 0;
|
||
let task = userData.task;
|
||
if (task) {
|
||
task = JSON.parse(task);
|
||
} else {
|
||
task = {
|
||
levelPass: {
|
||
value: 0,
|
||
state: 0,
|
||
},
|
||
share: {
|
||
value: 0,
|
||
state: 0,
|
||
},
|
||
useEnergy: {
|
||
value: 0,
|
||
state: 0,
|
||
},
|
||
useProp: {
|
||
value: 0,
|
||
state: 0,
|
||
}
|
||
}
|
||
}
|
||
const now = new Date(Date.now() + 8 * 3600000);
|
||
now.setHours(0, 0, 0, 0); // 直接修改当前Date对象
|
||
const timestamp = now.getTime();
|
||
if (timestamp > taskTime) {
|
||
for (var key in task) {
|
||
task[key].value = 0;
|
||
task[key].state = 0;
|
||
}
|
||
}
|
||
const loginUpdate: any = {
|
||
session_key: session_key,
|
||
...(unionid ? { unionid } : {}),
|
||
distinctId: distinctId,
|
||
isDebug: isDebug,
|
||
isFirst: false,
|
||
task: JSON.stringify(task),
|
||
// 新版配套时间写法(暂不执行):taskTime: Date.now(),
|
||
taskTime: Date.now() + 8 * 3600000,
|
||
token: token,
|
||
...migrationUpdate,
|
||
};
|
||
|
||
if (!Array.isArray(userData.catArr)) {
|
||
loginUpdate.catArr = [1, 2, 3];
|
||
}
|
||
|
||
if (userData.userCat == null || userData.userCat === "") {
|
||
loginUpdate.userCat = (Math.floor(Math.random() * 3) + 1) + "";
|
||
}
|
||
|
||
await db.collection(dbname)
|
||
.where({ _id: userData._id })
|
||
.update(loginUpdate);
|
||
|
||
if (shouldPersistABTests) {
|
||
try {
|
||
await db.collection(dbname)
|
||
.where({ _id: userData._id })
|
||
.update({ abTests: abTests });
|
||
} catch (e) {
|
||
console.log("AB测试分组写回失败", e);
|
||
}
|
||
}
|
||
}
|
||
await delayExecution(0.1);
|
||
userData = await getUserData(dbname, openid);
|
||
/* 新版每日/周任务登录后补齐逻辑暂不执行,避免将旧账号任务改写为 V2。
|
||
const ensuredTask = refreshTaskData(
|
||
userData.task,
|
||
userData._id,
|
||
Number(userData.levelAmount) + 1 || 1,
|
||
);
|
||
if (ensuredTask.changed) {
|
||
userData.task = JSON.stringify(ensuredTask.data);
|
||
userData.taskTime = Date.now();
|
||
await db.collection(dbname).where({ _id: userData._id }).update({
|
||
task: userData.task,
|
||
taskTime: userData.taskTime,
|
||
});
|
||
}
|
||
*/
|
||
let outTradeNo: any = [];
|
||
if (!isNewUser) {
|
||
let res = null;
|
||
if (userData.openid) {
|
||
res = await db.collection("order").where({ state: 1, openid: userData.openid }).get();
|
||
}
|
||
if (res.data && res.data.length > 0) {
|
||
for (let i = 0; i < res.data.length; i++) {
|
||
outTradeNo.push(res.data[i]);
|
||
}
|
||
}
|
||
if (dbname == "users") {
|
||
let ret = await getIosOrder(userData.openid, ctx);
|
||
outTradeNo = outTradeNo.concat(ret);
|
||
}
|
||
}
|
||
if (dbname == "users") {
|
||
// 旧客户端不处理新战令订单,其他商品补发保持原流程。
|
||
userData.outTradeNo = Number(ctx.body.passVersion) === 2 ? outTradeNo : outTradeNo.filter(o =>
|
||
!(o.passVersion === 2 || o.itemid === 'battlepass_30' || o.itemid === 'battlepass_12'));
|
||
}
|
||
|
||
if (!userData.onlyId) {
|
||
const onlyId = await allocateOnlyId(idname);
|
||
userData.onlyId = onlyId;
|
||
await db.collection(dbname).where({ _id: userData._id }).update({ session_key: session_key, onlyId: onlyId });
|
||
}
|
||
let share = userData.shareLv;
|
||
if (share) {
|
||
userData.shareLv = JSON.parse(share);
|
||
} else {
|
||
userData.shareLv = [];
|
||
}
|
||
userData.careerAmount = userData.careerAmount || 0;
|
||
if (dbname === "users") {
|
||
userData.abTests = abTests;
|
||
}
|
||
userData.rebornGiftCount = 0;
|
||
if (userData.isWhite && !userData.film && userData.film != 0) {
|
||
userData.film = userData.levelAmount;
|
||
}
|
||
userData.canIos = canIos;
|
||
userData.version = version;
|
||
if (dbname !== "users") {
|
||
delete userData.jungleTreasureState;
|
||
delete userData.jungleTreasureServerNow;
|
||
delete userData.pendingJungleSettlement;
|
||
} else if (Object.prototype.hasOwnProperty.call(userData, "jungleTreasureState")) {
|
||
// 仅随登录返回服务器时间,不在登录阶段创建或修改活动状态。
|
||
userData.jungleTreasureServerNow = Date.now();
|
||
}
|
||
if (wucaiPreview) {
|
||
userData.wucai_preview = wucaiPreview;
|
||
}
|
||
delete userData.isWuCai;
|
||
return { code: 1, data: userData }
|
||
}
|
||
async function allocateOnlyId(idname: string): Promise<number> {
|
||
if (!idname) {
|
||
throw new Error("计数器 ID 未配置,请检查 IDCOUNT_NAME");
|
||
}
|
||
const result: any = await cloud.mongo.db.collection<{ _id: string; count: number }>("idcount").findOneAndUpdate(
|
||
{ _id: idname },
|
||
{ $inc: { count: 1 } },
|
||
{ returnDocument: "before", projection: { count: 1, _id: 0 } }
|
||
);
|
||
// 兼容旧版驱动的 ModifyResult 和新版驱动直接返回的文档。
|
||
const counter = result && ("value" in result ? result.value : result);
|
||
if (!counter) {
|
||
throw new Error(`计数器不存在:${idname}`);
|
||
}
|
||
return counter.count;
|
||
}
|
||
|
||
function stringToHash(input: string, algorithm: 'sha1' | 'sha256' | 'sha512' = 'sha256'): string {
|
||
return createHash(algorithm).update(input).digest('hex');
|
||
}
|
||
|
||
async function hasUserOrder(openid: string): Promise<boolean> {
|
||
if (!openid) {
|
||
return false;
|
||
}
|
||
const result = await db.collection("order").where({ openid }).get();
|
||
return Array.isArray(result.data) && result.data.length > 0;
|
||
}
|
||
|
||
function wucaiLoginFail(errorCode: string, msg: string) {
|
||
return { code: 0, errorCode, data: null, msg };
|
||
}
|
||
|
||
function getTodayUTCMidnightTimestamp(): number {
|
||
const now = new Date();
|
||
// 1. 获取当前 UTC 时间
|
||
const utcMidnight = Date.UTC(
|
||
now.getUTCFullYear(),
|
||
now.getUTCMonth(),
|
||
now.getUTCDate(),
|
||
0, 0, 0, 0
|
||
);
|
||
// 2. 加上 8 小时(北京时间 = UTC+8)
|
||
return utcMidnight + 8 * 60 * 60 * 1000;
|
||
}
|
||
const getClientIp = (req) => {
|
||
return (
|
||
req.headers['x-forwarded-for']?.split(',')[0]?.trim() || // 代理链中的第一个 IP
|
||
req.headers['x-real-ip'] || // Nginx 配置的 real-ip
|
||
req.socket.remoteAddress // 原始 IP(可能是代理)
|
||
);
|
||
};
|
||
|
||
const propName = {
|
||
"金币包1": "gold_1",
|
||
"金币包2": "gold_2",
|
||
"金币包3": "gold_3",
|
||
"金币包4": "gold_4",
|
||
"金币包5": "gold_5",
|
||
"金币包6": "gold_6",
|
||
"无限体力组合包1": "unlimited_health_bundle_1",
|
||
"无限体力组合包2": "unlimited_health_bundle_2",
|
||
"无限体力组合包3": "unlimited_health_bundle_3",
|
||
"局内购买冻结": "freeze_in_game",
|
||
"局内购买锤子": "hammer_in_game",
|
||
"局内购买魔棒": "wand_in_gamerefill",
|
||
"补满体力": "health",
|
||
"宝藏1": "jungle_treasure_1",
|
||
"宝藏2": "jungle_treasure_2",
|
||
"宝藏3": "jungle_treasure_3",
|
||
"宝藏4": "jungle_treasure_4",
|
||
"宝藏5": "jungle_treasure_5",
|
||
"宝藏6": "jungle_treasure_6",
|
||
"宝藏7": "jungle_treasure_7",
|
||
"宝藏8": "jungle_treasure_8"
|
||
};
|
||
|
||
async function getWXOpenId(code, dbname) {
|
||
let appid = process.env.WX_MINIGAME_APP_ID;
|
||
let appsecret = process.env.WX_MINIGAME_APP_SECRET;
|
||
if (dbname == "usersAd") {
|
||
appid = "";
|
||
appsecret = "";
|
||
}
|
||
const api_url = `https://api.weixin.qq.com/sns/jscode2session`;
|
||
const param = `appid=${appid}&secret=${appsecret}&js_code=${code}&grant_type=authorization_code`;
|
||
let res = null;
|
||
try {
|
||
res = await cloud.fetch(`${api_url}?${param}`, {
|
||
headers: {
|
||
"Accept-Encoding": 'gzip'
|
||
}
|
||
});
|
||
} catch {
|
||
|
||
}
|
||
|
||
|
||
if (res == null || res.data.errcode > 0) {
|
||
return null
|
||
}
|
||
//console.log(res.data);
|
||
return [res?.data?.openid, res?.data?.session_key, res?.data?.unionid]
|
||
}
|
||
/**获取当前用户数据 */
|
||
async function getUserData(dbname: string, openid: any, isFirst?) {
|
||
let { data } = await db.collection(dbname).where({ openid: openid }).get();
|
||
if (data.length) {
|
||
return data[0];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
async function delayExecution(seconds: number) {
|
||
return new Promise((resolve) => {
|
||
setTimeout(() => {
|
||
// 在这里编写需要延时执行的代码
|
||
resolve("延时执行完成");
|
||
}, seconds * 1000);
|
||
});
|
||
}
|
||
async function getIosOrder(openid, ctx) {
|
||
let res = await db.collection("order").where({ state: 0, type: "ios", openid: openid }).get();
|
||
let res1 = await db.collection("users").where({ openid: openid }).getOne();
|
||
let list = [];
|
||
if (res.data.length > 0) {
|
||
for (let i = 0; i < res.data.length; i++) {
|
||
let ret = await getOrder(res.data[i].outTradeNo);
|
||
if (ret.trade_state == "SUCCESS") {
|
||
list.push(res.data[i]);
|
||
sead(res.data[i], res1.data, getClientIp(ctx));
|
||
const jungleTier = JungleConfig.getJunglePaidTierByProductId(res.data[i].itemid);
|
||
if (jungleTier) {
|
||
const unlockResult = JungleConfig.unlockJungleTierByProductId(
|
||
res1.data.jungleTreasureState,
|
||
res.data[i].itemid,
|
||
res.data[i].outTradeNo,
|
||
);
|
||
const userUpdate: any = { pay_user: true };
|
||
if (unlockResult.ok && unlockResult.changed && unlockResult.state) {
|
||
userUpdate.jungleTreasureState = JungleConfig.serializeJungleState(unlockResult.state);
|
||
res1.data.jungleTreasureState = unlockResult.state;
|
||
}
|
||
await db.collection("users").where({ _id: res1.data._id }).update(userUpdate);
|
||
} else switch (res.data[i].itemid) {
|
||
case "month_Card":
|
||
// 月卡有效期由 monthlyCard 使用订单号统一结算,避免补单与客户端重复加时。
|
||
await db.collection("users").where({ _id: res1.data._id }).update({ pay_user: true });
|
||
break;
|
||
case "reborn_Gift":
|
||
db.collection("users").where({ _id: res1.data._id }).update({ rebornGiftTime: Date.now(), rebornGiftCount: 0, pay_user: true });
|
||
break;
|
||
case "starter_pack":
|
||
db.collection("users").where({ _id: res1.data._id }).update({ starter_packState: 1, pay_user: true });
|
||
break;
|
||
default:
|
||
db.collection("users").where({ _id: res1.data._id }).update({ pay_user: true });
|
||
break;
|
||
}
|
||
// 月卡补单确认成功后进入待客户端补发状态。
|
||
if (res.data[i].itemid === "month_Card" || jungleTier || /^battlepass(?:_30|_12)?$/.test(res.data[i].itemid)) {
|
||
await db.collection("order").where({ outTradeNo: res.data[i].outTradeNo }).update({ state: 1 });
|
||
}
|
||
} else {
|
||
if (!res.data[i].passVersion && Date.now() - res.data[i].time > 3 * 3600000) {
|
||
let ret = await db.collection('order').where({ outTradeNo: res.data[i].outTradeNo }).remove({ multi: true });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return list;
|
||
}
|
||
async function sead(order, userInfo, ip) {
|
||
if (!userInfo.distinctId) {
|
||
return;
|
||
}
|
||
let id = '95993f9ab6f1402a87abe5147827e5e0';
|
||
if (userInfo.isDebug != "true") {
|
||
id = '40e3d5c5f2af49a4a074205564ce5dbb';
|
||
}
|
||
let teSDK = ThinkingData.initWithBatchMode(id, 'https://data.nika4fun.com/sync_data', {
|
||
dryRun: false, // report data to TE or not
|
||
deviceId: "123456789",//设备唯一标识
|
||
});
|
||
let trackEvent = {
|
||
accountId: order.openid,
|
||
distinctId: userInfo.distinctId,//访客id
|
||
event: 'payment',
|
||
time: new Date(Date.now() + 8 * 3600000),
|
||
ip: ip,
|
||
properties: {
|
||
order_id: order.outTradeNo,
|
||
pay_amount: order.goodsPrice,
|
||
payment_name: order.itemid,
|
||
payment_num: order.itemCount,
|
||
payment_type: "ios",
|
||
is_on_table: Number(userInfo.addLevel) >= 5,
|
||
current_level: userInfo.lv, //当前关卡等级 number
|
||
current_health: userInfo.healthAmount, //当前体力值
|
||
tmp_coin: userInfo.coinAmount,//当前金币
|
||
version: "1.6",
|
||
user_id: userInfo._id, //用户id
|
||
pay_user: userInfo.pay_user,
|
||
abTest_layer_one: getABTestGroupValue(userInfo.abTests, "layer_1"),
|
||
abTest_layer_two: getABTestGroupValue(userInfo.abTests, "layer_2"),
|
||
abTest_layer_three: getABTestGroupValue(userInfo.abTests, "layer_3")
|
||
},
|
||
callback(e) {
|
||
if (e) {
|
||
console.log(e);
|
||
}
|
||
}
|
||
};
|
||
teSDK.track(trackEvent);
|
||
teSDK.close();
|
||
return;
|
||
}
|
||
|
||
function getABTestGroupValue(abTests: any, layerKey: string) {
|
||
if (!abTests) {
|
||
return 0;
|
||
}
|
||
if (typeof abTests === "string") {
|
||
try {
|
||
abTests = JSON.parse(abTests);
|
||
} catch {
|
||
return 0;
|
||
}
|
||
}
|
||
const groupMap = { A: 1, B: 2, C: 3, D: 4, E: 5 };
|
||
const group = String(abTests[layerKey]?.group || "").toUpperCase();
|
||
return groupMap[group] || 0;
|
||
}
|
||
|
||
/**获取订单*/
|
||
async function getOrder(outTradeNo) {
|
||
const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=1719895437`;
|
||
const nonceStr = crypto.randomBytes(16).toString('hex');
|
||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||
let au = await sign(outTradeNo, nonceStr, timestamp);
|
||
let res = await fetch(url, {
|
||
method: 'GET', // 默认是 GET,可省略
|
||
headers: {
|
||
'Authorization': au, // 认证令牌
|
||
'Content-Type': 'application/json', // 声明请求体类型(GET 请求通常不需要)
|
||
'Accept': 'application/json', // 声明期望的响应类型
|
||
}
|
||
})
|
||
const data = await res.json();
|
||
return data;
|
||
}
|
||
async function sign(outTradeNo, nonceStr, timestamp) {
|
||
const data = `GET\n/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=1719895437\n${timestamp}\n${nonceStr}\n\n`
|
||
// 创建签名对象
|
||
const sign = crypto.createSign('RSA-SHA256');
|
||
// 更新签名内容
|
||
sign.update(data);
|
||
// 获取存储桶
|
||
const bucket = cloud.storage.bucket('j3k323ol-colorblock-oss');
|
||
// 获取文件列表
|
||
const res = await bucket.readFile('apiclient_key.pem');
|
||
const privateKeyPem = await res.Body.transformToString();
|
||
// 使用私钥签名(PKCS1 填充)
|
||
const signature = sign.sign({
|
||
key: privateKeyPem,
|
||
padding: crypto.constants.RSA_PKCS1_PADDING
|
||
}, 'base64');
|
||
// 构建Authorization头
|
||
const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${mchid}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${serialNo}"`;
|
||
return authorization;
|
||
}
|
||
|
||
async function getActiveABAssignments(openid: string, oldAbTests: any, isNewUser: boolean) {
|
||
const now = Date.now();
|
||
const abTests = normalizeABTests(oldAbTests, now);
|
||
let configs = [];
|
||
try {
|
||
const ret = await db.collection(AB_CONFIG_COLLECTION).get();
|
||
configs = pickConfigByLayer(ret.data || []);
|
||
} catch (e) {
|
||
return ensureManagedABLayers(abTests, now);
|
||
}
|
||
|
||
const configByLayer: any = {};
|
||
for (const config of configs) {
|
||
configByLayer[getABLayerKey(config.layer)] = config;
|
||
}
|
||
|
||
const nextAbTests: any = { ...abTests };
|
||
for (const layer of MANAGED_AB_LAYERS) {
|
||
const layerKey = getABLayerKey(layer);
|
||
const experiment = configByLayer[layerKey];
|
||
const oldAssignment = abTests[layerKey];
|
||
|
||
if (!experiment) {
|
||
nextAbTests[layerKey] = buildDefaultABAssignment(layer, null, oldAssignment, now, 0);
|
||
continue;
|
||
}
|
||
|
||
const resetVersion = getABResetVersion(experiment.resetVersion);
|
||
if (experiment.enabled === false) {
|
||
nextAbTests[layerKey] = buildDefaultABAssignment(
|
||
layer,
|
||
experiment,
|
||
oldAssignment,
|
||
now,
|
||
resetVersion
|
||
);
|
||
continue;
|
||
}
|
||
|
||
const shouldReset = experiment.resetAssignments === true
|
||
&& resetVersion > 0
|
||
&& getABResetVersion(oldAssignment?.appliedResetVersion) !== resetVersion;
|
||
if (isNewUser || shouldReset) {
|
||
const variant = pickABVariant(openid, experiment);
|
||
nextAbTests[layerKey] = variant
|
||
? buildABAssignment(layer, experiment, variant.group, now, resetVersion)
|
||
: buildDefaultABAssignment(layer, experiment, oldAssignment, now, resetVersion);
|
||
continue;
|
||
}
|
||
|
||
if (oldAssignment) {
|
||
nextAbTests[layerKey] = {
|
||
...oldAssignment,
|
||
experimentId: getABText(experiment.experimentId),
|
||
layer: layer,
|
||
name: getABText(experiment.name),
|
||
enabled: true,
|
||
};
|
||
} else {
|
||
nextAbTests[layerKey] = buildDefaultABAssignment(layer, experiment, null, now, 0);
|
||
}
|
||
}
|
||
return nextAbTests;
|
||
}
|
||
|
||
function ensureManagedABLayers(abTests: any, now: number) {
|
||
const completed: any = { ...abTests };
|
||
for (const layer of MANAGED_AB_LAYERS) {
|
||
const layerKey = getABLayerKey(layer);
|
||
if (!completed[layerKey]) {
|
||
completed[layerKey] = buildDefaultABAssignment(layer, null, null, now, 0);
|
||
}
|
||
}
|
||
return completed;
|
||
}
|
||
|
||
function buildDefaultABAssignment(layer: string, experiment: any, oldAssignment: any, now: number, resetVersion: number) {
|
||
const wasDefault = normalizeABGroup(oldAssignment?.group) === DEFAULT_AB_GROUP;
|
||
return {
|
||
experimentId: experiment ? getABText(experiment.experimentId) : null,
|
||
layer: layer,
|
||
name: experiment ? getABText(experiment.name) : null,
|
||
enabled: !!experiment && experiment.enabled !== false,
|
||
group: DEFAULT_AB_GROUP,
|
||
assignedAt: wasDefault ? oldAssignment.assignedAt : now,
|
||
appliedResetVersion: resetVersion,
|
||
};
|
||
}
|
||
|
||
function buildABAssignment(layer: string, experiment: any, group: any, assignedAt: number, resetVersion: number) {
|
||
return {
|
||
experimentId: getABText(experiment.experimentId),
|
||
layer: layer,
|
||
name: getABText(experiment.name),
|
||
enabled: true,
|
||
group: normalizeABGroup(group),
|
||
assignedAt: assignedAt,
|
||
appliedResetVersion: resetVersion,
|
||
};
|
||
}
|
||
|
||
function normalizeABTests(abTests: any, now = Date.now()) {
|
||
if (!abTests) {
|
||
return {};
|
||
}
|
||
if (typeof abTests == "string") {
|
||
try {
|
||
abTests = JSON.parse(abTests);
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
if (typeof abTests !== "object" || Array.isArray(abTests)) {
|
||
return {};
|
||
}
|
||
|
||
const normalized: any = {};
|
||
for (const key in abTests) {
|
||
const item = abTests[key];
|
||
const layer = String(item?.layer || "").trim();
|
||
const group = normalizeABGroup(item?.group);
|
||
if (!layer || !group || key !== getABLayerKey(layer)) {
|
||
continue;
|
||
}
|
||
const assignedAt = Number(item.assignedAt);
|
||
normalized[key] = {
|
||
experimentId: getABText(item.experimentId),
|
||
layer: layer,
|
||
name: getABText(item.name),
|
||
enabled: item.enabled !== false,
|
||
group: group,
|
||
assignedAt: Number.isFinite(assignedAt) && assignedAt > 0 ? assignedAt : now,
|
||
appliedResetVersion: getABResetVersion(item.appliedResetVersion),
|
||
};
|
||
}
|
||
return normalized;
|
||
}
|
||
|
||
function normalizeABGroup(group: any) {
|
||
const normalized = String(group || "").trim();
|
||
return normalized.toLowerCase() === "default" ? DEFAULT_AB_GROUP : normalized;
|
||
}
|
||
|
||
function getABText(value: any) {
|
||
if (value === null || value === undefined || value === "") {
|
||
return null;
|
||
}
|
||
return String(value);
|
||
}
|
||
|
||
function getABResetVersion(value: any) {
|
||
const version = Number(value);
|
||
return Number.isFinite(version) && version > 0 ? version : 0;
|
||
}
|
||
|
||
function getABLayerKey(layer: any) {
|
||
return `layer_${String(layer || "").trim()}`;
|
||
}
|
||
|
||
function pickConfigByLayer(configs: any[]) {
|
||
const byLayer: any = {};
|
||
for (let i = 0; i < configs.length; i++) {
|
||
const config = configs[i];
|
||
if (!isValidExperiment(config)) {
|
||
continue;
|
||
}
|
||
const layerKey = getABLayerKey(config.layer);
|
||
const old = byLayer[layerKey];
|
||
if (!old || Number(config.updatedAt || config.createdAt || 0) >= Number(old.updatedAt || old.createdAt || 0)) {
|
||
byLayer[layerKey] = config;
|
||
}
|
||
}
|
||
return Object.keys(byLayer).map(key => byLayer[key]);
|
||
}
|
||
|
||
function isValidExperiment(experiment: any) {
|
||
if (!experiment?.experimentId || !experiment?.layer || !Array.isArray(experiment?.variants)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function pickABVariant(openid: string, experiment: any) {
|
||
const variants = experiment.variants || [];
|
||
const totalRatio = variants.reduce((sum: number, variant: any) => sum + Number(variant.ratio || 0), 0);
|
||
if (totalRatio <= 0) {
|
||
return null;
|
||
}
|
||
const seed = `${openid}:${experiment.layer}:${experiment.experimentId}`;
|
||
const hash = createHash("sha256").update(seed).digest("hex");
|
||
const bucket = parseInt(hash.slice(0, 8), 16) % 10000;
|
||
const target = bucket / 10000 * totalRatio;
|
||
let cursor = 0;
|
||
for (let i = 0; i < variants.length; i++) {
|
||
cursor += Number(variants[i].ratio || 0);
|
||
if (target < cursor) {
|
||
return variants[i];
|
||
}
|
||
}
|
||
return variants[variants.length - 1];
|
||
}
|
||
/**
|
||
* 根据 IP 获取省份名称
|
||
* @param {string} ip - 用户 IP 地址
|
||
* @returns {string} 省份名称(如 "广东省"),未知时返回 "未知"
|
||
*/
|
||
function getProvinceByIP(ip) {
|
||
try {
|
||
const result = query.search(ip);
|
||
//console.log(result);
|
||
if (!result || !result.country.includes('中国')) {
|
||
return '其他';
|
||
}
|
||
|
||
// ip2region 返回的格式示例:"中国|0|广东省|深圳市|电信"
|
||
const parts = result.province;
|
||
const provinceCode = parts[2]; // 省份代码在第三部分
|
||
|
||
// 检查是否是已知的省份代码
|
||
// if (PROVCE_CODE_MAP[provinceCode]) {
|
||
// return PROVINCE_CODE_MAP[provinceCode];
|
||
// }
|
||
|
||
// 如果未映射,返回原始代码(可能是数据库更新后的新代码)
|
||
return parts || '其他';
|
||
} catch (error) {
|
||
console.error('IP 解析失败:', error);
|
||
return '其他';
|
||
}
|
||
}
|