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 = "1719895437";//商户id const serialNo = "2A97ED6ED441E55600EBC8830B9EC889ED41496E";//证书序列号 const ThinkingData = require('thinkingdata-node'); import Ip2region from 'ip2region'; const query = new Ip2region(); const canIos = false; const version = "9.9.99"; const AB_CONFIG_COLLECTION = "ab_config"; 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 = "69faba99b4d89b170da4bf56"; 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); if (userData?.freeze) { return { code: 0, data: "账号封禁", msg: "账号封禁" }; } let token = await stringToHash(Date.now() + openid); let abTests = {}; let wucaiPreview = null; if (!userData) { abTests = await getActiveABAssignments(openid, null); let migratedUser = null; if (from === "wucai") { if (await hasUserOrder(openid)) { return wucaiLoginFail("WUCAI_TARGET_AMBIGUOUS", "当前账号存在订单但用户记录缺失,请联系客服"); } const source = await fetchWucaiUser(unionid); if (!source) { return wucaiLoginFail("WUCAI_SOURCE_UNAVAILABLE", "五彩数据获取失败,请重新进入游戏后重试"); } migratedUser = normalizeWucaiUser(source); } let ref = await db.collection("idcount").field({ count: 1 }).where({ _id: idname }).getOne(); await db.collection("idcount").where({ _id: idname }).update({ count: ref.data.count + 1 }); await db.collection(dbname).add({ openid: openid, unionid: unionid, session_key: session_key, register_time: Date.now(), onlyId: ref.data.count, 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, 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, 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: number | null = hasStoredWucaiType ? Number(userData.wucai_type) : WUCAI_TYPE_NONE; if (!hasStoredWucaiType && userData.isWuCai === true) { if (supportsWucaiMigration) { wucaiType = WUCAI_TYPE_AUTO_MIGRATED; migrationUpdate.wucai_type = wucaiType; migrationUpdate.forcedUpdate = true; } else { wucaiType = null; } } else 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); } } abTests = normalizeABTests(userData.abTests); 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() + 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); } await delayExecution(0.1); userData = await getUserData(dbname, openid); let res = null; if (userData.openid) { res = await db.collection("order").where({ state: 1, openid: userData.openid }).get(); } let outTradeNo: any = []; 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); userData.outTradeNo = outTradeNo; } if (!userData.onlyId) { let ref = await db.collection("idcount").field({ count: 1 }).where({ _id: idname }).getOne(); await db.collection("idcount").where({ _id: idname }).update({ count: ref.data.count + 1 }); userData.onlyId = ref.data.count; await db.collection(dbname).where({ _id: userData._id }).update({ session_key: session_key, onlyId: ref.data.count }); } let share = userData.shareLv; if (share) { userData.shareLv = JSON.parse(share); } else { userData.shareLv = []; } userData.careerAmount = userData.careerAmount || 0; userData.abTests = normalizeABTests(abTests); let rebornGiftTime = userData?.rebornGiftTime || 0; let times = getTodayStartTimestamp(); if (rebornGiftTime < times) { userData.rebornGiftCount = 0; } if (userData.isWhite && !userData.film && userData.film != 0) { userData.film = userData.levelAmount; } userData.canIos = canIos; userData.version = version; 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 } } function stringToHash(input: string, algorithm: 'sha1' | 'sha256' | 'sha512' = 'sha256'): string { return createHash(algorithm).update(input).digest('hex'); } async function hasUserOrder(openid: string): Promise { 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" }; function getTodayStartTimestamp() { const date = new Date(); date.setHours(0, 0, 0, 0); return date.getTime(); } async function getWXOpenId(code, dbname) { let appid = "wx30f74367b3d3e7a6"; let appsecret = "a3417fe674feadb77809d8f9a365190d"; 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: 1, 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) { await db.collection("order").where({ outTradeNo: res.data[i].outTradeNo }).update({ state: 1 }); } } else { if (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 = '87d18958cea145f29d3265470ecd3486'; if (userInfo.isDebug != "true") { id = '121591378fc1423893deb12041413eb3'; } 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) { let abTests = normalizeABTests(oldAbTests); let configs = []; try { let ret = await db.collection(AB_CONFIG_COLLECTION).get(); // console.log("AB config count:", ret.data?.length || 0, ret.data); configs = pickConfigByLayer(ret.data || []); } catch (e) { // console.log("AB配置读取失败", e); return abTests; } const now = Date.now(); const nextAbTests: any = {}; for (let i = 0; i < configs.length; i++) { const experiment = configs[i]; if (!isValidExperiment(experiment)) { continue; } const layerKey = getABLayerKey(experiment.layer); if (abTests[layerKey] && experiment.resetAssignments !== true) { nextAbTests[layerKey] = { ...abTests[layerKey], experimentId: experiment.experimentId, layer: String(experiment.layer), name: experiment.name || abTests[layerKey].name || "", enabled: experiment.enabled !== false, }; continue; } const variant = pickABVariant(openid, experiment); if (!variant) { continue; } nextAbTests[layerKey] = { experimentId: experiment.experimentId, layer: String(experiment.layer), name: experiment.name || "", enabled: experiment.enabled !== false, group: variant.group, assignedAt: now, }; } return nextAbTests; } function normalizeABTests(abTests: any) { if (!abTests) { return {}; } if (typeof abTests == "string") { try { abTests = JSON.parse(abTests); } catch { return {}; } } const normalized: any = {}; for (const key in abTests) { const item = abTests[key]; if (!item || !item.layer || key !== getABLayerKey(item.layer)) { continue; } normalized[key] = { experimentId: item.experimentId || "", layer: String(item.layer), name: item.name || "", enabled: item.enabled !== false, group: item.group, assignedAt: item.assignedAt || Date.now(), }; } return normalized; } 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 '其他'; } }