refactor: 收口平台钱包余额 delta
This commit is contained in:
@@ -539,6 +539,11 @@ import {
|
||||
buildWoodenFishSessionFromWorkDetail,
|
||||
} from './platformMiniGameSessionMappingModel';
|
||||
import { resolvePlatformPlayedWorkOpenIntent } from './platformPlayedWorkOpenModel';
|
||||
import {
|
||||
adjustProfileDashboardWalletBalance,
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard,
|
||||
resolveProfileWalletBalance,
|
||||
} from './platformProfileWalletDeltaModel';
|
||||
import {
|
||||
type PlatformPublicCodeSearchStep,
|
||||
resolvePlatformPublicCodeSearchPlan,
|
||||
@@ -1049,60 +1054,6 @@ function openPuzzleRuntimeStage(
|
||||
writePuzzleRuntimeUrlState(state);
|
||||
}
|
||||
|
||||
function resolveProfileWalletBalance(
|
||||
dashboard: { walletBalance?: number | null } | null | undefined,
|
||||
) {
|
||||
const walletBalance = dashboard?.walletBalance;
|
||||
return typeof walletBalance === 'number' && Number.isFinite(walletBalance)
|
||||
? Math.max(0, Math.floor(walletBalance))
|
||||
: 0;
|
||||
}
|
||||
|
||||
function adjustProfileDashboardWalletBalance(
|
||||
dashboard: ProfileDashboardSummary | null,
|
||||
delta: number,
|
||||
): ProfileDashboardSummary | null {
|
||||
if (!dashboard || !Number.isFinite(delta) || delta === 0) {
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
return {
|
||||
...dashboard,
|
||||
walletBalance: Math.max(
|
||||
0,
|
||||
resolveProfileWalletBalance(dashboard) + Math.trunc(delta),
|
||||
),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previousDashboard: ProfileDashboardSummary | null,
|
||||
latestDashboard: ProfileDashboardSummary | null,
|
||||
localDelta: number,
|
||||
) {
|
||||
if (
|
||||
!previousDashboard ||
|
||||
!latestDashboard ||
|
||||
!Number.isFinite(localDelta) ||
|
||||
localDelta === 0
|
||||
) {
|
||||
return Number.isFinite(localDelta) ? Math.trunc(localDelta) : 0;
|
||||
}
|
||||
|
||||
const previousBalance = resolveProfileWalletBalance(previousDashboard);
|
||||
const latestBalance = resolveProfileWalletBalance(latestDashboard);
|
||||
const normalizedDelta = Math.trunc(localDelta);
|
||||
|
||||
if (normalizedDelta < 0) {
|
||||
const reflectedDebit = Math.max(0, previousBalance - latestBalance);
|
||||
return Math.min(0, normalizedDelta + reflectedDebit);
|
||||
}
|
||||
|
||||
const reflectedCredit = Math.max(0, latestBalance - previousBalance);
|
||||
return Math.max(0, normalizedDelta - reflectedCredit);
|
||||
}
|
||||
|
||||
function isPuzzleFormOnlyDraft(session: PuzzleAgentSessionSnapshot | null) {
|
||||
return Boolean(
|
||||
session?.stage === 'collecting_anchors' && session.draft?.formDraft,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { ProfileDashboardSummary } from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
adjustProfileDashboardWalletBalance,
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard,
|
||||
resolveProfileWalletBalance,
|
||||
} from './platformProfileWalletDeltaModel';
|
||||
|
||||
const NOW = Date.parse('2026-06-04T04:30:00.000Z');
|
||||
|
||||
function buildDashboard(
|
||||
overrides: Partial<ProfileDashboardSummary> = {},
|
||||
): ProfileDashboardSummary {
|
||||
return {
|
||||
walletBalance: 100,
|
||||
totalPlayTimeMs: 0,
|
||||
playedWorldCount: 0,
|
||||
updatedAt: '2026-06-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(NOW);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('platformProfileWalletDeltaModel', () => {
|
||||
test('normalizes wallet balance to a non-negative integer', () => {
|
||||
expect(resolveProfileWalletBalance(buildDashboard({ walletBalance: 12.8 }))).toBe(
|
||||
12,
|
||||
);
|
||||
expect(
|
||||
resolveProfileWalletBalance(buildDashboard({ walletBalance: -4 })),
|
||||
).toBe(0);
|
||||
expect(resolveProfileWalletBalance({ walletBalance: Number.NaN })).toBe(0);
|
||||
expect(resolveProfileWalletBalance(null)).toBe(0);
|
||||
});
|
||||
|
||||
test('applies local delta and refreshes dashboard timestamp', () => {
|
||||
expect(
|
||||
adjustProfileDashboardWalletBalance(buildDashboard(), -3.8),
|
||||
).toMatchObject({
|
||||
walletBalance: 97,
|
||||
updatedAt: '2026-06-04T04:30:00.000Z',
|
||||
});
|
||||
expect(
|
||||
adjustProfileDashboardWalletBalance(buildDashboard({ walletBalance: 2 }), -10),
|
||||
).toMatchObject({
|
||||
walletBalance: 0,
|
||||
});
|
||||
expect(adjustProfileDashboardWalletBalance(null, 5)).toBeNull();
|
||||
const dashboard = buildDashboard();
|
||||
expect(adjustProfileDashboardWalletBalance(dashboard, Number.POSITIVE_INFINITY)).toBe(
|
||||
dashboard,
|
||||
);
|
||||
});
|
||||
|
||||
test('reconciles debit delta already reflected by latest server dashboard', () => {
|
||||
const previous = buildDashboard({ walletBalance: 100 });
|
||||
expect(
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previous,
|
||||
buildDashboard({ walletBalance: 98 }),
|
||||
-5,
|
||||
),
|
||||
).toBe(-3);
|
||||
expect(
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previous,
|
||||
buildDashboard({ walletBalance: 92 }),
|
||||
-5,
|
||||
),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('reconciles credit delta already reflected by latest server dashboard', () => {
|
||||
const previous = buildDashboard({ walletBalance: 100 });
|
||||
expect(
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previous,
|
||||
buildDashboard({ walletBalance: 103 }),
|
||||
8,
|
||||
),
|
||||
).toBe(5);
|
||||
expect(
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previous,
|
||||
buildDashboard({ walletBalance: 120 }),
|
||||
8,
|
||||
),
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
test('does not reconcile when server balance moves against local delta', () => {
|
||||
const previous = buildDashboard({ walletBalance: 100 });
|
||||
expect(
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previous,
|
||||
buildDashboard({ walletBalance: 104 }),
|
||||
-5,
|
||||
),
|
||||
).toBe(-5);
|
||||
expect(
|
||||
reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previous,
|
||||
buildDashboard({ walletBalance: 96 }),
|
||||
8,
|
||||
),
|
||||
).toBe(8);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { ProfileDashboardSummary } from '../../../packages/shared/src/contracts/runtime';
|
||||
|
||||
type ProfileWalletBalanceSource =
|
||||
| Pick<ProfileDashboardSummary, 'walletBalance'>
|
||||
| { walletBalance?: number | null }
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export function resolveProfileWalletBalance(
|
||||
dashboard: ProfileWalletBalanceSource,
|
||||
) {
|
||||
const walletBalance = dashboard?.walletBalance;
|
||||
return typeof walletBalance === 'number' && Number.isFinite(walletBalance)
|
||||
? Math.max(0, Math.floor(walletBalance))
|
||||
: 0;
|
||||
}
|
||||
|
||||
export function adjustProfileDashboardWalletBalance(
|
||||
dashboard: ProfileDashboardSummary | null,
|
||||
delta: number,
|
||||
): ProfileDashboardSummary | null {
|
||||
if (!dashboard || !Number.isFinite(delta) || delta === 0) {
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
return {
|
||||
...dashboard,
|
||||
walletBalance: Math.max(
|
||||
0,
|
||||
resolveProfileWalletBalance(dashboard) + Math.trunc(delta),
|
||||
),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function reconcileProfileWalletLocalDeltaWithServerDashboard(
|
||||
previousDashboard: ProfileDashboardSummary | null,
|
||||
latestDashboard: ProfileDashboardSummary | null,
|
||||
localDelta: number,
|
||||
) {
|
||||
if (
|
||||
!previousDashboard ||
|
||||
!latestDashboard ||
|
||||
!Number.isFinite(localDelta) ||
|
||||
localDelta === 0
|
||||
) {
|
||||
return Number.isFinite(localDelta) ? Math.trunc(localDelta) : 0;
|
||||
}
|
||||
|
||||
const previousBalance = resolveProfileWalletBalance(previousDashboard);
|
||||
const latestBalance = resolveProfileWalletBalance(latestDashboard);
|
||||
const normalizedDelta = Math.trunc(localDelta);
|
||||
|
||||
if (normalizedDelta < 0) {
|
||||
const reflectedDebit = Math.max(0, previousBalance - latestBalance);
|
||||
return Math.min(0, normalizedDelta + reflectedDebit);
|
||||
}
|
||||
|
||||
const reflectedCredit = Math.max(0, latestBalance - previousBalance);
|
||||
return Math.max(0, normalizedDelta - reflectedCredit);
|
||||
}
|
||||
Reference in New Issue
Block a user