平衡:世界传音时间降低

This commit is contained in:
taojiacheng committed 2026-07-03 14:55:20 +08:00
1 parent d33ba46b59
commit b4b6cb7b9f
4 files changed
+91 -5

No files matched your search

@@ -0,0 +1,60 @@
const { redisSetMock, redisTtlMock } = vi.hoisted(() => ({
redisSetMock: vi.fn(),
redisTtlMock: vi.fn(),
}));
vi.mock('./index', () => ({
redis: {
set: redisSetMock,
ttl: redisTtlMock,
},
}));
import {
checkAndAcquireCooldown,
getWorldChatCooldownSeconds,
} from './worldChatLimiter';
describe('worldChatLimiter', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('scales cooldown from 60 seconds to 15 seconds by major realm', () => {
expect(getWorldChatCooldownSeconds('炼气')).toBe(60);
expect(getWorldChatCooldownSeconds('筑基')).toBeLessThan(60);
expect(getWorldChatCooldownSeconds('渡劫')).toBe(15);
expect(getWorldChatCooldownSeconds('unknown')).toBe(60);
});
it('uses the realm-scaled cooldown as the redis lock ttl', async () => {
redisSetMock.mockResolvedValue('OK');
await expect(
checkAndAcquireCooldown('cultivator-1', '渡劫'),
).resolves.toEqual({
allowed: true,
remainingSeconds: 0,
});
expect(redisSetMock).toHaveBeenCalledWith(
'world_chat:cooldown:cultivator-1',
'1',
'EX',
15,
'NX',
);
});
it('falls back to the realm-scaled cooldown when redis ttl is unavailable', async () => {
redisSetMock.mockResolvedValue(null);
redisTtlMock.mockResolvedValue(-1);
await expect(
checkAndAcquireCooldown('cultivator-1', '炼气'),
).resolves.toEqual({
allowed: false,
remainingSeconds: 60,
});
});
});
+23 -4
View File
@@ -1,17 +1,36 @@
import { REALM_ORDER, type RealmType } from '@shared/types/constants';
import { redis } from './index';
const CHAT_COOLDOWN_SECONDS = 60;
const MAX_CHAT_COOLDOWN_SECONDS = 60;
const MIN_CHAT_COOLDOWN_SECONDS = 15;
function getCooldownKey(cultivatorId: string): string {
return `world_chat:cooldown:${cultivatorId}`;
}
export async function checkAndAcquireCooldown(cultivatorId: string): Promise<{
export function getWorldChatCooldownSeconds(realm: RealmType | string): number {
const highestRealmOrder = Math.max(...Object.values(REALM_ORDER));
const currentRealmOrder =
realm in REALM_ORDER ? REALM_ORDER[realm as RealmType] : 0;
const cooldownRange = MAX_CHAT_COOLDOWN_SECONDS - MIN_CHAT_COOLDOWN_SECONDS;
const reduction = (cooldownRange * currentRealmOrder) / highestRealmOrder;
return Math.max(
MIN_CHAT_COOLDOWN_SECONDS,
Math.round(MAX_CHAT_COOLDOWN_SECONDS - reduction),
);
}
export async function checkAndAcquireCooldown(
cultivatorId: string,
realm: RealmType | string,
): Promise<{
allowed: boolean;
remainingSeconds: number;
}> {
const cooldownSeconds = getWorldChatCooldownSeconds(realm);
const key = getCooldownKey(cultivatorId);
const result = await redis.set(key, '1', 'EX', CHAT_COOLDOWN_SECONDS, 'NX');
const result = await redis.set(key, '1', 'EX', cooldownSeconds, 'NX');
if (result === 'OK') {
return {
@@ -22,7 +41,7 @@ export async function checkAndAcquireCooldown(cultivatorId: string): Promise<{
const ttl = await redis.ttl(key);
const remainingSeconds =
typeof ttl === 'number' && ttl > 0 ? ttl : CHAT_COOLDOWN_SECONDS;
typeof ttl === 'number' && ttl > 0 ? ttl : cooldownSeconds;
return {
allowed: false,
@@ -195,6 +195,10 @@ describe('world chat router', () => {
});
expect(response.status).toBe(200);
expect(checkAndAcquireCooldownMock).toHaveBeenCalledWith(
'cultivator-1',
'foundation',
);
expect(createMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
channel: 'world',
+4 -1
View File
@@ -168,7 +168,10 @@ router.post(
}
const parsed = getValidatedJson<WorldChatCreateMessageRequest>(c);
const cooldown = await checkAndAcquireCooldown(cultivator.id);
const cooldown = await checkAndAcquireCooldown(
cultivator.id,
cultivator.realm,
);
if (!cooldown.allowed) {
return c.json(
{