Skip to content

Commit 51fd156

Browse files
team qpm limit & plan tracks (#6066)
* team qpm limit & plan tracks * api entry qpm * perf: computed days * Revert "api entry qpm" This reverts commit 1210c07. * perf: code * system qpm limit * system qpm limit --------- Co-authored-by: archer <545436317@qq.com>
1 parent 744fc92 commit 51fd156

File tree

7 files changed

+172
-66
lines changed

7 files changed

+172
-66
lines changed

packages/global/common/middle/tracks/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ export enum TrackEnum {
1010
readSystemAnnouncement = 'readSystemAnnouncement',
1111
clickOperationalAd = 'clickOperationalAd',
1212
closeOperationalAd = 'closeOperationalAd',
13-
teamChatQPM = 'teamChatQPM'
13+
teamChatQPM = 'teamChatQPM',
14+
subscriptionDeleted = 'subscriptionDeleted',
15+
freeAccountCleanup = 'freeAccountCleanup'
1416
}

packages/service/common/api/frequencyLimit.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,50 @@
22
import { getGlobalRedisConnection } from '../../common/redis';
33
import { jsonRes } from '../../common/response';
44
import type { NextApiResponse } from 'next';
5+
import { teamQPM } from '../../support/wallet/sub/utils';
6+
import z from 'zod';
7+
import { addLog } from 'common/system/log';
58

69
export enum LimitTypeEnum {
710
chat = 'chat'
811
}
9-
const limitMap = {
10-
[LimitTypeEnum.chat]: {
11-
seconds: 60,
12-
limit: Number(process.env.CHAT_MAX_QPM || 5000)
12+
13+
const FrequencyLimitOptionSchema = z.union([
14+
z.object({
15+
type: z.literal(LimitTypeEnum.chat),
16+
teamId: z.string()
17+
})
18+
]);
19+
type FrequencyLimitOption = z.infer<typeof FrequencyLimitOptionSchema>;
20+
21+
const getLimitData = async (data: FrequencyLimitOption) => {
22+
if (data.type === LimitTypeEnum.chat) {
23+
const qpm = await teamQPM.getTeamQPMLimit(data.teamId);
24+
25+
if (!qpm) return;
26+
27+
return {
28+
limit: qpm,
29+
seconds: 60
30+
};
1331
}
32+
return;
1433
};
1534

16-
type FrequencyLimitOption = {
17-
teamId: string;
18-
type: LimitTypeEnum;
19-
res: NextApiResponse;
20-
};
35+
/*
36+
true: 未达到限制
37+
false: 达到了限制
38+
*/
39+
export const teamFrequencyLimit = async ({
40+
teamId,
41+
type,
42+
res
43+
}: FrequencyLimitOption & { res: NextApiResponse }) => {
44+
const data = await getLimitData({ type, teamId });
45+
if (!data) return true;
46+
47+
const { limit, seconds } = data;
2148

22-
export const teamFrequencyLimit = async ({ teamId, type, res }: FrequencyLimitOption) => {
23-
const { seconds, limit } = limitMap[type];
2449
const redis = getGlobalRedisConnection();
2550
const key = `frequency:${type}:${teamId}`;
2651

@@ -31,13 +56,16 @@ export const teamFrequencyLimit = async ({ teamId, type, res }: FrequencyLimitOp
3156
.exec();
3257

3358
if (!result) {
34-
return Promise.reject(new Error('Redis connection error'));
59+
return true;
3560
}
3661

3762
const currentCount = result[0][1] as number;
3863

3964
if (currentCount > limit) {
4065
const remainingTime = await redis.ttl(key);
66+
addLog.info(
67+
`[Completion Limit] Team ${teamId} reached the limit of ${limit} requests per ${seconds} seconds. Remaining time: ${remainingTime} seconds.`
68+
);
4169
jsonRes(res, {
4270
code: 429,
4371
error: `Rate limit exceeded. Maximum ${limit} requests per ${seconds} seconds for this team. Please try again in ${remainingTime} seconds.`

packages/service/common/middle/tracks/utils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
88
import { getAppLatestVersion } from '../../../core/app/version/controller';
99
import { type ShortUrlParams } from '@fastgpt/global/support/marketing/type';
1010
import { getRedisCache, setRedisCache } from '../../redis/cache';
11+
import { differenceInDays } from 'date-fns';
1112

1213
const createTrack = ({ event, data }: { event: TrackEnum; data: Record<string, any> }) => {
1314
if (!global.feConfigs?.isPlus) return;
@@ -156,5 +157,35 @@ export const pushTrack = {
156157
teamId: data.teamId
157158
}
158159
});
160+
},
161+
subscriptionDeleted: (data: {
162+
teamId: string;
163+
subscriptionType: string;
164+
subLevel?: string;
165+
totalPoints: number;
166+
usedPoints: number;
167+
startTime: Date;
168+
expiredTime: Date;
169+
}) => {
170+
return createTrack({
171+
event: TrackEnum.subscriptionDeleted,
172+
data: {
173+
teamId: data.teamId,
174+
subscriptionType: data.subscriptionType,
175+
subLevel: data.subLevel,
176+
totalPoints: data.totalPoints,
177+
usedPoints: data.usedPoints,
178+
activeDays: differenceInDays(data.expiredTime, data.startTime)
179+
}
180+
});
181+
},
182+
freeAccountCleanup: (data: { teamId: string; expiredTime: Date }) => {
183+
return createTrack({
184+
event: TrackEnum.freeAccountCleanup,
185+
data: {
186+
teamId: data.teamId,
187+
expiredTime: data.expiredTime
188+
}
189+
});
159190
}
160191
};

packages/service/common/redis/cache.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ const getCacheKey = (key: string) => `${redisPrefix}${key}`;
88
export enum CacheKeyEnum {
99
team_vector_count = 'team_vector_count',
1010
team_point_surplus = 'team_point_surplus',
11-
team_point_total = 'team_point_total'
11+
team_point_total = 'team_point_total',
12+
team_qpm_limit = 'team_qpm_limit'
1213
}
1314

1415
// Seconds
1516
export enum CacheKeyEnumTime {
1617
team_vector_count = 30 * 60,
1718
team_point_surplus = 1 * 60,
18-
team_point_total = 1 * 60
19+
team_point_total = 1 * 60,
20+
team_qpm_limit = 60 * 60
1921
}
2022

2123
export const setRedisCache = async (

packages/service/common/system/timerLock/constants.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export enum TimerIdEnum {
1010
clearExpiredRawTextBuffer = 'clearExpiredRawTextBuffer',
1111
clearExpiredDatasetImage = 'clearExpiredDatasetImage',
1212
clearExpiredMinioFiles = 'clearExpiredMinioFiles',
13-
recordTeamQPM = 'recordTeamQPM'
13+
recordTeamQPM = 'recordTeamQPM',
14+
auditLogCleanup = 'auditLogCleanup',
15+
chatHistoryCleanup = 'chatHistoryCleanup'
1416
}
1517

1618
export enum LockNotificationEnum {

packages/service/support/permission/teamLimit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getTeamPlanStatus, getTeamStandPlan, getTeamPoints } from '../../support/wallet/sub/utils';
1+
import { getTeamPlanStatus, getTeamStandPlan, teamPoint } from '../../support/wallet/sub/utils';
22
import { MongoApp } from '../../core/app/schema';
33
import { MongoDataset } from '../../core/dataset/schema';
44
import { DatasetTypeEnum } from '@fastgpt/global/core/dataset/constants';
@@ -12,7 +12,7 @@ import { getVectorCountByTeamId } from '../../common/vectorDB/controller';
1212
export const checkTeamAIPoints = async (teamId: string) => {
1313
if (!global.subPlans?.standard) return;
1414

15-
const { totalPoints, usedPoints } = await getTeamPoints({ teamId });
15+
const { totalPoints, usedPoints } = await teamPoint.getTeamPoints({ teamId });
1616

1717
if (usedPoints >= totalPoints) {
1818
return Promise.reject(TeamErrEnum.aiPointsNotEnough);

packages/service/support/wallet/sub/utils.ts

Lines changed: 89 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export const getTeamPlanStatus = async ({
190190
? standardPlans[standardPlan.currentSubLevel]
191191
: undefined;
192192

193-
updateTeamPointsCache({ teamId, totalPoints, surplusPoints });
193+
teamPoint.updateTeamPointsCache({ teamId, totalPoints, surplusPoints });
194194

195195
return {
196196
[SubTypeEnum.standard]: standardPlan,
@@ -223,58 +223,99 @@ export const getTeamPlanStatus = async ({
223223
};
224224
};
225225

226-
export const clearTeamPointsCache = async (teamId: string) => {
227-
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
228-
const totalCacheKey = `${CacheKeyEnum.team_point_total}:${teamId}`;
229-
230-
await Promise.all([delRedisCache(surplusCacheKey), delRedisCache(totalCacheKey)]);
231-
};
232-
233-
export const incrTeamPointsCache = async ({ teamId, value }: { teamId: string; value: number }) => {
234-
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
235-
await incrValueToCache(surplusCacheKey, value);
236-
};
237-
export const updateTeamPointsCache = async ({
238-
teamId,
239-
totalPoints,
240-
surplusPoints
241-
}: {
242-
teamId: string;
243-
totalPoints: number;
244-
surplusPoints: number;
245-
}) => {
246-
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
247-
const totalCacheKey = `${CacheKeyEnum.team_point_total}:${teamId}`;
226+
export const teamPoint = {
227+
getTeamPoints: async ({ teamId }: { teamId: string }) => {
228+
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
229+
const totalCacheKey = `${CacheKeyEnum.team_point_total}:${teamId}`;
230+
231+
const [surplusCacheStr, totalCacheStr] = await Promise.all([
232+
getRedisCache(surplusCacheKey),
233+
getRedisCache(totalCacheKey)
234+
]);
235+
236+
if (surplusCacheStr && totalCacheStr) {
237+
const totalPoints = Number(totalCacheStr);
238+
const surplusPoints = Number(surplusCacheStr);
239+
return {
240+
totalPoints,
241+
surplusPoints,
242+
usedPoints: totalPoints - surplusPoints
243+
};
244+
}
248245

249-
await Promise.all([
250-
setRedisCache(surplusCacheKey, surplusPoints, CacheKeyEnumTime.team_point_surplus),
251-
setRedisCache(totalCacheKey, totalPoints, CacheKeyEnumTime.team_point_total)
252-
]);
246+
const planStatus = await getTeamPlanStatus({ teamId });
247+
return {
248+
totalPoints: planStatus.totalPoints,
249+
surplusPoints: planStatus.totalPoints - planStatus.usedPoints,
250+
usedPoints: planStatus.usedPoints
251+
};
252+
},
253+
incrTeamPointsCache: async ({ teamId, value }: { teamId: string; value: number }) => {
254+
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
255+
await incrValueToCache(surplusCacheKey, value);
256+
},
257+
updateTeamPointsCache: async ({
258+
teamId,
259+
totalPoints,
260+
surplusPoints
261+
}: {
262+
teamId: string;
263+
totalPoints: number;
264+
surplusPoints: number;
265+
}) => {
266+
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
267+
const totalCacheKey = `${CacheKeyEnum.team_point_total}:${teamId}`;
268+
269+
await Promise.all([
270+
setRedisCache(surplusCacheKey, surplusPoints, CacheKeyEnumTime.team_point_surplus),
271+
setRedisCache(totalCacheKey, totalPoints, CacheKeyEnumTime.team_point_total)
272+
]);
273+
},
274+
clearTeamPointsCache: async (teamId: string) => {
275+
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
276+
const totalCacheKey = `${CacheKeyEnum.team_point_total}:${teamId}`;
277+
278+
await Promise.all([delRedisCache(surplusCacheKey), delRedisCache(totalCacheKey)]);
279+
}
253280
};
281+
export const teamQPM = {
282+
getTeamQPMLimit: async (teamId: string): Promise<number | null> => {
283+
// 1. 尝试从缓存中获取
284+
const cacheKey = `${CacheKeyEnum.team_qpm_limit}:${teamId}`;
285+
const cached = await getRedisCache(cacheKey);
286+
287+
if (cached) {
288+
return Number(cached);
289+
}
254290

255-
export const getTeamPoints = async ({ teamId }: { teamId: string }) => {
256-
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
257-
const totalCacheKey = `${CacheKeyEnum.team_point_total}:${teamId}`;
291+
// 2. Computed
292+
const teamPlanStatus = await getTeamPlanStatus({ teamId });
293+
const limit =
294+
teamPlanStatus[SubTypeEnum.standard]?.requestsPerMinute ??
295+
teamPlanStatus.standardConstants?.requestsPerMinute;
258296

259-
const [surplusCacheStr, totalCacheStr] = await Promise.all([
260-
getRedisCache(surplusCacheKey),
261-
getRedisCache(totalCacheKey)
262-
]);
297+
if (!limit) {
298+
if (process.env.CHAT_MAX_QPM) return Number(process.env.CHAT_MAX_QPM);
299+
return null;
300+
}
263301

264-
if (surplusCacheStr && totalCacheStr) {
265-
const totalPoints = Number(totalCacheStr);
266-
const surplusPoints = Number(surplusCacheStr);
267-
return {
268-
totalPoints,
269-
surplusPoints,
270-
usedPoints: totalPoints - surplusPoints
271-
};
302+
// 3. Set cache
303+
await teamQPM.setCachedTeamQPMLimit(teamId, limit);
304+
305+
return limit;
306+
},
307+
setCachedTeamQPMLimit: async (teamId: string, limit: number): Promise<void> => {
308+
const cacheKey = `${CacheKeyEnum.team_qpm_limit}:${teamId}`;
309+
await setRedisCache(cacheKey, limit.toString(), CacheKeyEnumTime.team_qpm_limit);
310+
},
311+
clearTeamQPMLimitCache: async (teamId: string): Promise<void> => {
312+
const cacheKey = `${CacheKeyEnum.team_qpm_limit}:${teamId}`;
313+
await delRedisCache(cacheKey);
272314
}
315+
};
273316

274-
const planStatus = await getTeamPlanStatus({ teamId });
275-
return {
276-
totalPoints: planStatus.totalPoints,
277-
surplusPoints: planStatus.totalPoints - planStatus.usedPoints,
278-
usedPoints: planStatus.usedPoints
279-
};
317+
// controler
318+
export const clearTeamPlanCache = async (teamId: string) => {
319+
await teamPoint.clearTeamPointsCache(teamId);
320+
await teamQPM.clearTeamQPMLimitCache(teamId);
280321
};

0 commit comments

Comments
 (0)