MatchMaster/server/laf-cloud/functions/login.ts
2026-08-13 20:08:01 +08:00

609 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import cloud from '@lafjs/cloud'
import { createHash } from 'crypto';
import * as JungleConfigModule from "@/jungleConfig";
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) {
let code = ctx.body.code;
let distinctId = ctx.body.distinctId;
let isDebug = ctx.body.isDebug;
let dbname = "users";
const gameName = ctx.body.gameName;
if (gameName == "iaa") {
dbname = "usersAd";
}
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);
let token = await stringToHash(Date.now() + openid);
let abTests = {};
if (!userData) {
abTests = await getActiveABAssignments(openid, null);
// let user = await getUserInfo(unionid);
let user = null;
let isWuCai = false;
if (user) {
isWuCai = true;
}
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: user?.levelAmount || 0,
coinAmount: user?.coinAmount || 0,
healthAmount: 5,
healthtimestamp: 0,
freezeAmount: user?.freezeAmount || 3,
hammerAmount: user?.hammerAmount || 3,
magicAmount: user?.magicAmount || 3,
userPowerTime: user?.userPowerTime || 0,
monthCardReward: user?.monthCardReward || 0,
monthCardTime: user?.monthCardTime || 0,
rebornGiftCount: 0,
rebornGiftTime: 0,
useravatar: Math.floor(Math.random() * 4) + "",
username: "user",
pay_user: user?.pay_user || false,
isWhite: false,
isFirst: true,
starter_pack: user?.starter_pack || 0,
starter_packState: user?.starter_packState || 0,
winStreak: user?.winStreak || 0,
passCheck: user?.passCheck || null,
endLevelNum: user?.endLevelNum || 0,
totalLevel: user?.totalLevel || 0,
userLevel: user?.userLevel || 0,
film: user?.film || 0,
careerAmount: 0,
useravatarIcon: "0",
userCat: (Math.floor(Math.random() * 3) + 1) + "",
forcedUpdate: false,
isWuCai: isWuCai,
headArr: user?.headArr || [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
catArr: user?.catArr || [1, 2, 3],
token: token,
abTests: abTests
});
} else {
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,
distinctId: distinctId,
isDebug: isDebug,
isFirst: false,
task: JSON.stringify(task),
taskTime: Date.now() + 8 * 3600000,
token: token
};
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 (userData?.freeze) {
return { code: 0, data: "账号封禁", msg: "账号封禁" };
}
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();
}
return { code: 1, data: userData }
}
function stringToHash(input: string, algorithm: 'sha1' | 'sha256' | 'sha512' = 'sha256'): string {
return createHash(algorithm).update(input).digest('hex');
}
async function getUserInfo(unionid) {
const res = await fetch("https://laf.nika4games.com/getUserInfoByUnionid", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ unionid: unionid })
})
const token1 = await res.json()
return token1?.data || null;
}
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 '其他';
}
}