Files
server/laf-cloud/functions/userLevel.ts
T
2026-10-21 11:11:17 +08:00

386 lines
14 KiB
TypeScript

import { createHash } from 'crypto';
import * as GoldMinerServiceModule from "@/goldMiner/service";
import * as GoldMinerRulesModule from "@/goldMiner/config";
const GoldMinerService: any = GoldMinerServiceModule;
const GoldMinerRules: any = GoldMinerRulesModule;
import cloud from '@lafjs/cloud';
const db = cloud.database();
import Utils from "@/Utils";
import Ip2region from 'ip2region';
const query = new Ip2region();
export default async function (ctx: FunctionContext) {
const action = ctx.body.action;
const uid = ctx.body.uid;
let dbname = "users";
// const gameName = ctx.body.gameName;
const isWuXian = ctx.body.isWuXian;
let db1 = process.env.SRANK_ID;
// if (gameName == "iaa") {
// dbname = "usersAd";
// db1 = "69fabd418463a95668c5e5d3";
// }
if (!action) {
return { code: 0, data: null, msg: "未获取到action" };
}
if (!uid) {
return { code: 0, data: null, msg: "未获取到uid" };
}
let res1 = await db.collection(dbname).where({ _id: uid }).getOne();
const token = ctx.body.token;
if (res1.data && res1.data.token) {
let istoken = Utils.checkToken(token, res1.data.token);
if (!istoken) {
return { code: 0, data: null, msg: "token校验失败" };;
}
}
let address = res1.data?.address;
const ip = ctx.headers['x-forwarded-for'] || ctx.headers['x-real-ip'] || '';
let address1 = getProvinceByIP(ip);
if (!address || address == "其他") {
address = address1;
}
if (["香港", "澳门", "台湾省"].includes(address)) {
address = "港澳台";
}
switch (action) {
case 'save':
let levelAmount = Number(ctx.body.levelAmount);
if (!levelAmount) {
return { code: 0, data: null, msg: "未获取到levelAmount" };
}
let oldlevelAmount = res1.data?.levelAmount || 0;
let oldAdd = res1.data?.addLevel || 0;
let levelTJ = res1.data?.levelTJ || 0;
let count = levelAmount - oldlevelAmount;
let goldMiner: any;
if (ctx.body.goldMiner != null) {
try {
GoldMinerService.authorize(res1.data, ctx.body);
const event = GoldMinerRules.parseObject(ctx.body.goldMiner);
const mode = isWuXian === true || isWuXian === "true" ? "endless" : "main";
if (event.mode !== mode) GoldMinerRules.reject("INVALID_INPUT");
goldMiner = { code: 1, ...await GoldMinerService.recordProgress(uid, { ...res1.data, levelAmount }, event) };
} catch (error) {
goldMiner = GoldMinerRules.failure(error);
}
}
if (count > 20 || count < -20) {
count = 1;
return { code: 1, data: { ...(goldMiner ? { goldMiner } : {}), IP: ip, address: address, levelAmount: levelAmount, timestamp: Date.now() }, msg: "用户数据更新成功" };
}
// 前15关只推进主线;保留已有生涯累计,补报跨过15关时只计算门槛之后的胜利。
const careerCount = Math.max(levelAmount - 15, 0) - Math.max(oldlevelAmount - 15, 0);
let add = oldAdd + careerCount;
let isWhite = res1.data?.isWhite;
let careerAmount = res1.data?.careerAmount || 0;
if (isWuXian == "true") {
add += 1;
//console.log("无线增加值:"+(add-oldAdd));
} else {
//console.log("非无线增加值:"+(add-oldAdd));
}
add = Math.floor(add);
let rankNum = -1;
if (add >= 5 && isWhite == false) {
rankNum = await rank(db1, uid, add, levelAmount, res1, address, levelTJ);
sendRank(uid, res1.data?.useravatar, res1.data?.username, res1.data?.useravatarIcon, res1.data?.userCat, add, levelTJ, address, res1.data?.unionid);
levelTJ = 1;
careerAmount += 1;
}
let film = res1.data.film;
if (!film && film != 0) {
film = res1.data.levelAmount;
}
film += count;
let ret = await db.collection(dbname).where({ _id: uid }).update({ address: address, film: film, careerAmount: careerAmount, levelTJ: levelTJ, levelAmount: levelAmount, addLevel: add, timestamp: Date.now() });
await recordWeeklyProgress(uid, count, ret);
if (ctx.body.goldMiner != null) {
try {
GoldMinerService.authorize(res1.data, ctx.body);
const event = GoldMinerRules.parseObject(ctx.body.goldMiner);
const mode = isWuXian === true || isWuXian === "true" ? "endless" : "main";
if (event.mode !== mode) GoldMinerRules.reject("INVALID_INPUT");
goldMiner = { code: 1, ...await GoldMinerService.recordProgress(uid, { ...res1.data, levelAmount }, event) };
} catch (error) {
goldMiner = GoldMinerRules.failure(error);
}
}
return { code: 1, data: { ...(goldMiner ? { goldMiner } : {}), IP: ip, address: address, rank: rankNum, levelAmount: levelAmount, film: film, timestamp: Date.now() }, msg: "用户数据更新成功" }
case 'read':
return {
code: 1, data: { levelAmount: res1.data?.levelAmount || 0, film: res1.data?.film ?? res1.data?.levelAmount ?? 0, timestamp: res1.data?.timestamp || 0 }, msg: "成功"
}
default:
return {
code: 400,
message: '无效的操作类型,请使用 save 或 read'
}
}
}
async function sendRank(uid, useravatar, username, useravatarIcon, userCat, count, levelTJ, address, unionid) {
let res = await fetch("https://bx7gi3d4oq.sealoshzh.site/userLevel", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uid: uid,
unionid: unionid,
useravatar: useravatar,
username: username,
useravatarIcon: useravatarIcon,
userCat: userCat,
count: count,
levelTJ: levelTJ,
address: address
})
})
const token1 = await res.json()
return token1;
}
function isSameDayUTC(timestamp1, timestamp2) {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
return date1.getUTCFullYear() === date2.getUTCFullYear() &&
date1.getUTCMonth() === date2.getUTCMonth() &&
date1.getUTCDate() === date2.getUTCDate();
}
async function rank(db1, uid, add, levelAmount, res1, address, levelTJ) {
let rankInfo = await db.collection("sRank").where({ _id: db1 }).getOne();
let total = rankInfo.data.total;
if (total != null) {
total = JSON.parse(total);
} else {
total = {};
}
if (total["all"]) {
let isHave = false;
for (let i = 0; i < total["all"].length; i++) {
if (total["all"][i].id == uid) {
isHave = true;
total["all"][i].useravatar = res1.data?.useravatar,
total["all"][i].username = res1.data?.username,
total["all"][i].useravatarIcon = res1.data?.useravatarIcon,
total["all"][i].userCat = res1.data?.userCat,
total["all"][i].addLevel = add;
total["all"][i].time = Date.now();
break;
}
}
if (isHave == false) {
let length = total["all"].length;
if (length < 10 || add > total["all"][length - 1].addLevel) {
total["all"].push({
id: uid,
addLevel: add,
time: Date.now(),
username: res1.data?.username,
useravatar: res1.data?.useravatar,
useravatarIcon: res1.data?.useravatarIcon,
userCat: res1.data?.userCat,
levelAmount: levelAmount
});
}
}
} else {
total["all"] = [{
id: uid,
addLevel: add,
time: Date.now(),
username: res1.data?.username,
useravatar: res1.data?.useravatar,
useravatarIcon: res1.data?.useravatarIcon,
userCat: res1.data?.userCat,
levelAmount: levelAmount
}];
}
total["all"].sort((a, b) => {
if (a.addLevel !== b.addLevel) {
return b.addLevel - a.addLevel;
}
return a.time - b.time;
});
if (total["all"].length >= 10) {
total["all"] = total["all"].slice(0, 10);
}
if (total[address]) {
let isHave = false;
for (let i = 0; i < total[address].length; i++) {
if (total[address][i].id == uid) {
isHave = true;
total[address][i].useravatar = res1.data?.useravatar,
total[address][i].username = res1.data?.username,
total[address][i].useravatarIcon = res1.data?.useravatarIcon,
total[address][i].userCat = res1.data?.userCat,
total[address][i].addLevel = add;
total[address][i].time = Date.now();
break;
}
}
if (isHave == false) {
let length = total[address].length;
if (length < 30 || add > total[address][length - 1].addLevel) {
total[address].push({
id: uid,
addLevel: add,
time: Date.now(),
username: res1.data?.username,
useravatar: res1.data?.useravatar,
useravatarIcon: res1.data?.useravatarIcon,
userCat: res1.data?.userCat,
levelAmount: levelAmount
});
}
}
} else {
total[address] = [{
id: uid,
addLevel: add,
time: Date.now(),
username: res1.data?.username,
useravatar: res1.data?.useravatar,
useravatarIcon: res1.data?.useravatarIcon,
userCat: res1.data?.userCat,
levelAmount: levelAmount
}];
}
total[address].sort((a, b) => {
if (a.addLevel !== b.addLevel) {
return b.addLevel - a.addLevel;
}
return a.time - b.time;
});
if (total[address].length >= 30) {
total[address] = total[address].slice(0, 30);
}
if (!total["role"]) {
total["role"] = {};
}
if (levelTJ == 0) {
if (!total["role"][address]) {
total["role"][address] = 1;
} else {
total["role"][address] += 1;
//console.log(address+"真实:"+total[address].length+"----"+total["role"][address]);
let length = total[address].length;
if (length < 10) {
total["role"][address] = length;
}
}
}
//console.log(address);
let ret1 = await db.collection("sRank").where({ _id: db1 }).update({ total: JSON.stringify(total) });
let rank = -1;
for (let i = 0; i < total[address].length; i++) {
if (total[address][i].id == uid) {
rank = i + 1;
break;
}
}
return rank;
}
/**
* 根据 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 '其他';
}
}
/** Monday 09:00 Asia/Shanghai. weekId identifies the period, not midnight. */
function progressWeek(at: number) {
const shifted = new Date(at - 3600000);
const startAt = Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth(), shifted.getUTCDate())
- ((shifted.getUTCDay() + 6) % 7) * 86400000 + 3600000;
return { weekId: new Date(startAt).toISOString().slice(0, 10), startAt, endAt: startAt + 7 * 86400000 };
}
/** Only confirmed legacy saves add positive integral deltas. Do not retry a write
* after an ambiguous error: its $inc may already have reached the database. */
async function recordWeeklyProgress(uid: string, delta: number, saved: any) {
if (!saved || saved.ok === false || saved.error || saved.updated !== 1) return;
if (!Number.isSafeInteger(delta) || delta <= 0 || delta > 20) return;
try {
const environment = process.env.PAYMENT_APP_ENV === 'test' ? 'test' : 'production';
const _id = createHash('sha256').update(JSON.stringify(['weeklyCompetition', environment, uid])).digest('hex');
const table = (cloud.mongo.db as any).collection('weekly_competition');
for (let attempt = 0; attempt < 12; attempt++) {
const week = progressWeek(Date.now());
const row = await table.findOne({ _id });
if (!row) {
try {
// Match the existing competition document shape, without joining a round.
await table.insertOne({
_id, uid, environment, revision: 0, current: null, pending: null, receipts: {},
progress: { weekId: week.weekId, weeklyWins: 0, previousWeeklyWins: 0 }
});
} catch (error: any) {
if (error.code !== 11000) throw error;
// Another request initialized the same document. No score was added here.
}
continue;
}
const progress = row.progress;
const beforeCutoff = { $lt: ['$$NOW', new Date(week.endAt)] };
if (progress?.weekId === week.weekId) {
const result = await table.updateOne({ _id, 'progress.weekId': week.weekId, $expr: beforeCutoff },
{ $inc: { 'progress.weeklyWins': delta } });
if (result.matchedCount === 1) return;
if (result.matchedCount === 0) continue; // Confirmed no write: safe to reread the week.
throw new Error('weekly progress increment result is unknown');
}
let previousWeeklyWins = 0;
const condition: any = { _id, $expr: beforeCutoff };
if (progress == null) {
condition.progress = progress === undefined ? { $exists: false } : null;
} else {
const oldStartAt = Date.parse(progress.weekId + 'T01:00:00Z');
if (!Number.isFinite(oldStartAt) || oldStartAt >= week.startAt
|| !Number.isSafeInteger(progress.weeklyWins) || progress.weeklyWins < 0) {
throw new Error('weekly progress period or score is invalid');
}
if (week.startAt - oldStartAt === 7 * 86400000) previousWeeklyWins = progress.weeklyWins;
// Include the old score so a concurrent old-week $inc cannot be overwritten.
condition['progress.weekId'] = progress.weekId;
condition['progress.weeklyWins'] = progress.weeklyWins;
}
const rotated = await table.updateOne(condition,
{ $set: { progress: { weekId: week.weekId, weeklyWins: 0, previousWeeklyWins } } });
if (rotated.matchedCount !== 0 && rotated.matchedCount !== 1) {
throw new Error('weekly progress rollover result is unknown');
}
// Reread after initialization, rotation or a known conditional-update conflict.
}
throw new Error('weekly progress conditional updates exhausted');
} catch (error) {
console.error('weekly progress recording failed; legacy save unchanged', { uid, delta, error });
}
}