refactor: read onboarding AB assignments within server functions

This commit is contained in:
COMPUTER\EDY 2026-09-11 15:11:43 +08:00
parent 365b224a47
commit c3894186c9
5 changed files with 37 additions and 23 deletions

View File

@ -1,5 +1,4 @@
import cloud from '@lafjs/cloud'
import Utils from "@/Utils";
import { createHash } from 'crypto';
const db = cloud.database();
@ -47,7 +46,14 @@ export function getChinaDateKey(now = Date.now()) {
}
export function getUserSignInTriggerLevel(user: any, config: SignInActivityConfig): number {
return Utils.isNewPlayerB(user) ? 15 : Number(config.triggerLevel);
let assignments = user?.abTests;
if (typeof assignments === "string") {
try { assignments = JSON.parse(assignments); } catch { assignments = null; }
}
const assignment = assignments?.layer_1;
const isNewPlayerB = !!assignment && assignment.name === "new1_50" && assignment.enabled !== false
&& ["B", "2"].includes(String(assignment.group).toUpperCase());
return isNewPlayerB ? 15 : Number(config.triggerLevel);
}
export function getUserSignInLevel(user: any) {

View File

@ -1,16 +1,6 @@
import cloud from '@lafjs/cloud';
const db = cloud.database();
export default class Utils {
static isNewPlayerB(user: any): boolean {
let assignments = user?.abTests;
if (typeof assignments === "string") {
try { assignments = JSON.parse(assignments); } catch { return false; }
}
const assignment = assignments?.layer_1;
return !!assignment && assignment.name === "new1_50" && assignment.enabled !== false
&& ["B", "2"].includes(String(assignment.group).toUpperCase());
}
static checkToken(token, token1) {
if (token == token1) {
//console.error("校验成功")

View File

@ -53,7 +53,14 @@ export default async function (ctx: FunctionContext) {
return;
}
// B组前15关只推进主线;补报跨过15关时也只计算门槛之后的胜利。
const careerCount = Utils.isNewPlayerB(res1.data)
let assignments = res1.data?.abTests;
if (typeof assignments === "string") {
try { assignments = JSON.parse(assignments); } catch { assignments = null; }
}
const assignment = assignments?.layer_1;
const isNewPlayerB = !!assignment && assignment.name === "new1_50" && assignment.enabled !== false
&& ["B", "2"].includes(String(assignment.group).toUpperCase());
const careerCount = isNewPlayerB
? Math.max(levelAmount - 15, 0) - Math.max(oldlevelAmount - 15, 0)
: count;
let add = oldAdd + careerCount;

View File

@ -31,7 +31,7 @@ function withNewPlayerConfig(config) {
return config;
}
const ServerUtils = loadMembers('server/laf-cloud/functions/Utils.ts', ['isNewPlayerB']);
const ServerUtils = loadMembers('server/laf-cloud/functions/Utils.ts', ['checkToken']);
function loadModule(file, deps = {}, globals = {}) {
const exports = {};
const code = ts.transpileModule(fs.readFileSync(file, 'utf8'), {

View File

@ -12,6 +12,9 @@ const config = (level = 0, group = 'B') => withNewPlayerConfig({ GM_INFO: {
const json = file => JSON.parse(fs.readFileSync(file, 'utf8'));
test('front and back agree on B/2; A, missing, disabled and unrelated experiments keep defaults', () => {
const service = loadModule('server/laf-cloud/functions/SignInActivity.ts', {
'@lafjs/cloud': { default: { database: () => ({}) } }, crypto: require('node:crypto'),
});
for (const [raw, expected] of [
[assignment('B'), true], [assignment(2), true], [assignment('2'), true],
[assignment('A'), false], [assignment(1), false], [assignment('1'), false],
@ -21,10 +24,10 @@ test('front and back agree on B/2; A, missing, disabled and unrelated experiment
]) {
const c = config(); c.GM_INFO.abTestAssignments = raw;
assert.equal(c.isNewPlayerB(), expected);
assert.equal(ServerUtils.isNewPlayerB({ abTests: raw }), expected);
assert.equal(ServerUtils.isNewPlayerB({ abTests: JSON.stringify(raw) }), expected);
assert.equal(service.getUserSignInTriggerLevel({ abTests: raw }, { triggerLevel: 23 }), expected ? 15 : 23);
assert.equal(service.getUserSignInTriggerLevel({ abTests: JSON.stringify(raw) }, { triggerLevel: 23 }), expected ? 15 : 23);
}
assert.equal(ServerUtils.isNewPlayerB({ abTests: 'invalid' }), false);
assert.equal(service.getUserSignInTriggerLevel({ abTests: 'invalid' }, { triggerLevel: 23 }), 23);
});
test('B mainline switches at 50/51; A, help and endless keep Json', async () => {
@ -144,11 +147,19 @@ test('front-end Career is hidden without counting through 15 and reused node app
});
test('server counts only victories after 15, including batches crossing the boundary', async () => {
for (const [group, oldLevel, level, expected] of [
const cases = [
['B', 0, 1, 0], ['B', 14, 15, 0], ['B', 15, 16, 1], ['B', 13, 18, 3],
['B', 15, 20, 5], ['B', 18, 18, 0], ['B', 50, 51, 1], ['A', 14, 15, 1], [0, 0, 1, 1],
]) {
const user = { _id: 'user', levelAmount: oldLevel, addLevel: 0, abTests: assignment(group), film: 100, isWhite: true, address: '浙江' };
[2, 14, 15, 0], ['2', 15, 16, 1],
].map(([group, oldLevel, level, expected]) => [assignment(group), oldLevel, level, expected]);
cases.push(
[JSON.stringify(assignment('B')), 14, 15, 0], [JSON.stringify(assignment('A')), 14, 15, 1],
[undefined, 14, 15, 1], ['invalid', 14, 15, 1],
[{ layer_1: { name: 'another', group: 'B' } }, 14, 15, 1],
[{ layer_1: { name: 'new1_50', group: 'B', enabled: false } }, 14, 15, 1],
);
for (const [abTests, oldLevel, level, expected] of cases) {
const user = { _id: 'user', levelAmount: oldLevel, addLevel: 0, abTests, film: 100, isWhite: true, address: '浙江' };
let update;
const cloud = { database: () => ({ collection: () => ({ where: () => ({ getOne: async () => ({ data: user }),
update: async payload => { update = payload; return { updated: 1 }; } }) }) }) };
@ -158,7 +169,7 @@ test('server counts only victories after 15, including batches crossing the boun
}, { process: { env: { SRANK_ID: 'rank' } } }).default;
const result = await api({ body: { uid: 'user', action: 'save', levelAmount: level }, headers: {} });
assert.equal(result.code, 1);
assert.equal(update.addLevel, expected, `${group}: ${oldLevel} -> ${level}`);
assert.equal(update.addLevel, expected, `${JSON.stringify(abTests)}: ${oldLevel} -> ${level}`);
assert.equal(update.film, 100 + level - oldLevel, 'film still follows all cleared levels');
assert.equal(update.levelAmount, level);
}
@ -166,7 +177,7 @@ test('server counts only victories after 15, including batches crossing the boun
test('seven-day activity uses B=15 while other players keep the configured trigger', () => {
const service = loadModule('server/laf-cloud/functions/SignInActivity.ts', {
'@lafjs/cloud': { default: { database: () => ({}) } }, '@/Utils': { default: ServerUtils }, crypto: require('node:crypto'),
'@lafjs/cloud': { default: { database: () => ({}) } }, crypto: require('node:crypto'),
});
for (const group of ['A', 'B', 0, '2']) {
assert.equal(service.getUserSignInTriggerLevel({ abTests: assignment(group) }, { triggerLevel: 23 }), ['B', '2'].includes(group) ? 15 : 23);
@ -179,7 +190,7 @@ test('B seven-day activity activates at cleared level 15 and retains the origina
get: async () => ({ data: [activity] }), update: async () => ({ updated: 1 }),
}) }) }) };
const service = loadModule('server/laf-cloud/functions/SignInActivity.ts', {
'@lafjs/cloud': { default: cloud }, '@/Utils': { default: ServerUtils }, crypto: require('node:crypto'),
'@lafjs/cloud': { default: cloud }, crypto: require('node:crypto'),
});
for (const [group, level, expected] of [['B', 14, false], ['B', 15, true], ['A', 15, false], ['A', 23, true]]) {
const result = await service.activateSignInActivity({ _id: 'player', levelAmount: level, abTests: assignment(group) });