From 347cc6f3d04bc199cdbec468d58fb43bddcc9981 Mon Sep 17 00:00:00 2001 From: guanchao <18612113337@163.com> Date: Wed, 21 Oct 2026 11:11:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=91=A8=E7=AB=9E=E8=B5=9B=E9=A2=84?= =?UTF-8?q?=E9=87=87=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- laf-cloud/functions/setUserLevel.ts | 74 ++++++++++++++++++++++++++++- laf-cloud/functions/userLevel.ts | 72 ++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) diff --git a/laf-cloud/functions/setUserLevel.ts b/laf-cloud/functions/setUserLevel.ts index 6f37fa4..c97491e 100644 --- a/laf-cloud/functions/setUserLevel.ts +++ b/laf-cloud/functions/setUserLevel.ts @@ -1,3 +1,4 @@ +import { createHash } from 'crypto'; import cloud from '@lafjs/cloud' const db = cloud.database(); import Utils from "@/Utils"; @@ -45,7 +46,8 @@ export default async function (ctx: FunctionContext) { return; } let totalLevel = userLevel + (endLevelNum / 10000) - await db.collection(dbname).where({ _id: uid }).update({ userLevel: userLevel, endLevelNum: endLevelNum, totalLevel: totalLevel }); + const saved = await db.collection(dbname).where({ _id: uid }).update({ userLevel: userLevel, endLevelNum: endLevelNum, totalLevel: totalLevel }); + await recordWeeklyProgress(uid, endLevelNum - Number(endLevelNum1), saved); return { code: 1, data: { userLevel: userLevel, endLevelNum: endLevelNum }, msg: "用户数据更新成功" } case 'read': let userLevel1 = res.data?.userLevel; @@ -60,3 +62,73 @@ export default async function (ctx: FunctionContext) { } } } + +/** 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 }); + } +} diff --git a/laf-cloud/functions/userLevel.ts b/laf-cloud/functions/userLevel.ts index 4e335b5..5e31f87 100644 --- a/laf-cloud/functions/userLevel.ts +++ b/laf-cloud/functions/userLevel.ts @@ -1,3 +1,4 @@ +import { createHash } from 'crypto'; import * as GoldMinerServiceModule from "@/goldMiner/service"; import * as GoldMinerRulesModule from "@/goldMiner/config"; const GoldMinerService: any = GoldMinerServiceModule; @@ -94,6 +95,7 @@ export default async function (ctx: FunctionContext) { } 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); @@ -311,3 +313,73 @@ function getProvinceByIP(ip) { 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 }); + } +}