平衡:世界传音时间降低
This commit is contained in:
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user