refactor: merge standardConstants and standard in team plan (#6549)

* refactor: merge standardConstants and standard in team plan

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

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: remove type assertion

* chore: type

* test: test buildStandardPlan

* fix: type

* perf: code perf

* add test code

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
Finley Ge
2026-03-15 22:13:43 +08:00
committed by GitHub
parent 007ca09772
commit 21b3f8549a
30 changed files with 1992 additions and 245 deletions
+30 -3
View File
@@ -2,7 +2,9 @@ import z from 'zod';
import { StandardSubLevelEnum, SubModeEnum, SubTypeEnum } from './constants';
import { ObjectIdSchema } from '../../../common/type/mongo';
// Content of plan
/**
* Static plan config, stored in global.subPlans
*/
export const TeamStandardSubPlanItemSchema = z.object({
name: z.string().optional(),
desc: z.string().optional(),
@@ -71,6 +73,10 @@ export const SubPlanSchema = z.object({
});
export type SubPlanType = z.infer<typeof SubPlanSchema>;
/**
* TeamSub Schema in DB.
* Configs are optional
*/
export const TeamSubSchema = z.object({
_id: ObjectIdSchema,
teamId: ObjectIdSchema,
@@ -105,9 +111,30 @@ export const TeamSubSchema = z.object({
});
export type TeamSubSchemaType = z.infer<typeof TeamSubSchema>;
/**
* Merged plan type: combines DB subscription record metadata with effective plan limits
*
* Omits:
* - maxApp/maxDataset from TeamSubSchema: 这些字段在 DB 中存储,但在合并后的类型中使用 maxAppAmount/maxDatasetAmount
* - pointPrice from TeamStandardSubPlanItemSchema: 避免与 price 字段冲突
*
* Field priority: TeamStandardSubPlanItemSchema fields override TeamSubSchema fields when both exist
*/ export const TeamPlanStandardSchema = z.object({
...TeamSubSchema.omit({
maxApp: true,
maxDataset: true
}).shape,
...TeamStandardSubPlanItemSchema.omit({
pointPrice: true,
price: true
}).shape,
price: z.number().optional()
});
export type TeamPlanStandardType = z.infer<typeof TeamPlanStandardSchema>;
export const TeamPlanStatusSchema = z.object({
[SubTypeEnum.standard]: TeamSubSchema.optional(),
standardConstants: TeamStandardSubPlanItemSchema.optional(),
[SubTypeEnum.standard]: TeamPlanStandardSchema.optional(),
totalPoints: z.int(),
usedPoints: z.int(),
datasetMaxSize: z.int()
@@ -25,7 +25,7 @@ export const checkTeamAIPoints = async (teamId: string) => {
};
export const checkTeamMemberLimit = async (teamId: string, newCount: number) => {
const [{ standardConstants }, memberCount] = await Promise.all([
const [{ standard }, memberCount] = await Promise.all([
getTeamStandPlan({
teamId
}),
@@ -35,7 +35,7 @@ export const checkTeamMemberLimit = async (teamId: string, newCount: number) =>
})
]);
if (standardConstants && newCount + memberCount > standardConstants.maxTeamMember) {
if (standard?.maxTeamMember && newCount + memberCount > standard.maxTeamMember) {
return Promise.reject(TeamErrEnum.teamOverSize);
}
};
@@ -50,7 +50,7 @@ export const checkTeamAppTypeLimit = async ({
amount?: number;
}) => {
if (appCheckType === 'app') {
const [{ standardConstants }, appCount] = await Promise.all([
const [{ standard }, appCount] = await Promise.all([
getTeamStandPlan({ teamId }),
MongoApp.countDocuments({
teamId,
@@ -60,7 +60,7 @@ export const checkTeamAppTypeLimit = async ({
})
]);
if (standardConstants && appCount + amount > standardConstants.maxAppAmount) {
if (standard?.maxAppAmount && appCount + amount > standard.maxAppAmount) {
return Promise.reject(TeamErrEnum.appAmountNotEnough);
}
@@ -107,10 +107,10 @@ export const checkDatasetIndexLimit = async ({
teamId: string;
insertLen?: number;
}) => {
const [{ standardConstants, totalPoints, usedPoints, datasetMaxSize }, usedDatasetIndexSize] =
const [{ standard, totalPoints, usedPoints, datasetMaxSize }, usedDatasetIndexSize] =
await Promise.all([getTeamPlanStatus({ teamId }), getVectorCountByTeamId(teamId)]);
if (!standardConstants) return;
if (!standard) return;
if (usedDatasetIndexSize + insertLen >= datasetMaxSize) {
return Promise.reject(TeamErrEnum.datasetSizeNotEnough);
@@ -123,7 +123,7 @@ export const checkDatasetIndexLimit = async ({
};
export const checkTeamDatasetLimit = async (teamId: string) => {
const [{ standardConstants }, datasetCount] = await Promise.all([
const [{ standard }, datasetCount] = await Promise.all([
getTeamStandPlan({ teamId }),
MongoDataset.countDocuments({
teamId,
@@ -132,7 +132,7 @@ export const checkTeamDatasetLimit = async (teamId: string) => {
]);
// User check
if (standardConstants && datasetCount >= standardConstants.maxDatasetAmount) {
if (standard?.maxDatasetAmount && datasetCount >= standard.maxDatasetAmount) {
return Promise.reject(TeamErrEnum.datasetAmountNotEnough);
}
@@ -148,11 +148,11 @@ export const checkTeamDatasetLimit = async (teamId: string) => {
};
export const checkTeamDatasetSyncPermission = async (teamId: string) => {
const { standardConstants } = await getTeamStandPlan({
const { standard } = await getTeamStandPlan({
teamId
});
if (standardConstants && !standardConstants?.websiteSyncPerDataset) {
if (standard && !standard?.websiteSyncPerDataset) {
return Promise.reject(TeamErrEnum.websiteSyncNotEnough);
}
};
+73 -105
View File
@@ -5,9 +5,11 @@ import {
standardSubLevelMap
} from '@fastgpt/global/support/wallet/sub/constants';
import { MongoTeamSub } from './schema';
import {
type TeamPlanStatusType,
type TeamSubSchemaType
import type {
TeamStandardSubPlanItemType,
TeamPlanStatusType,
TeamPlanStandardType,
TeamSubSchemaType
} from '@fastgpt/global/support/wallet/sub/type';
import dayjs from 'dayjs';
import { type ClientSession } from '../../../common/mongo';
@@ -38,55 +40,33 @@ export const sortStandPlans = (plans: TeamSubSchemaType[]) => {
standardSubLevelMap[b.currentSubLevel].weight - standardSubLevelMap[a.currentSubLevel].weight
);
};
export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => {
const plans = await MongoTeamSub.find(
{
teamId,
type: SubTypeEnum.standard
},
undefined,
{
...readFromSecondary
}
);
sortStandPlans(plans);
const standardPlans = global.subPlans?.standard;
const standard = plans[0];
const standardConstants =
standard.currentSubLevel && standardPlans
? standardPlans[
standard.currentSubLevel === StandardSubLevelEnum.custom
? StandardSubLevelEnum.advanced
: standard.currentSubLevel
]
: undefined;
return {
[SubTypeEnum.standard]: standard,
standardConstants: standardConstants
? {
...standardConstants,
maxTeamMember: standard?.maxTeamMember ?? standardConstants.maxTeamMember,
maxAppAmount: standard?.maxApp ?? standardConstants.maxAppAmount,
maxDatasetAmount: standard?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute: standard?.requestsPerMinute ?? standardConstants.requestsPerMinute,
chatHistoryStoreDuration:
standard?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
maxDatasetSize: standard?.maxDatasetSize ?? standardConstants.maxDatasetSize,
websiteSyncPerDataset:
standard?.websiteSyncPerDataset ?? standardConstants.websiteSyncPerDataset,
appRegistrationCount:
standard?.appRegistrationCount ?? standardConstants.appRegistrationCount,
auditLogStoreDuration:
standard?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
ticketResponseTime: standard?.ticketResponseTime ?? standardConstants.ticketResponseTime,
customDomain: standard?.customDomain ?? standardConstants.customDomain
}
: undefined
};
};
export const buildStandardPlan = (
standard: TeamSubSchemaType,
standardConstants: TeamStandardSubPlanItemType
): TeamPlanStandardType => ({
...standard,
name: standardConstants.name,
desc: standardConstants.desc,
price: standardConstants.price,
priceDescription: standardConstants.priceDescription,
customFormUrl: standardConstants.customFormUrl,
customDescriptions: standardConstants.customDescriptions,
wecom: standardConstants.wecom,
maxTeamMember: standard?.maxTeamMember ?? standardConstants.maxTeamMember,
maxAppAmount: standard?.maxApp ?? standardConstants.maxAppAmount,
maxDatasetAmount: standard?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute: standard?.requestsPerMinute ?? standardConstants.requestsPerMinute,
chatHistoryStoreDuration:
standard?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
maxDatasetSize: standard?.maxDatasetSize ?? standardConstants.maxDatasetSize,
websiteSyncPerDataset: standard?.websiteSyncPerDataset ?? standardConstants.websiteSyncPerDataset,
appRegistrationCount: standard?.appRegistrationCount ?? standardConstants.appRegistrationCount,
auditLogStoreDuration: standard?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
ticketResponseTime: standard?.ticketResponseTime ?? standardConstants.ticketResponseTime,
customDomain: standard?.customDomain ?? standardConstants.customDomain,
maxUploadFileSize: standard?.maxUploadFileSize ?? standardConstants.maxUploadFileSize,
maxUploadFileCount: standard?.maxUploadFileCount ?? standardConstants.maxUploadFileCount
});
export const initTeamFreePlan = async ({
teamId,
@@ -176,11 +156,46 @@ export const initTeamFreePlan = async ({
);
};
// 获取团队标准套餐
export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => {
const plans = await MongoTeamSub.find(
{
teamId,
type: SubTypeEnum.standard
},
undefined,
{
...readFromSecondary
}
);
sortStandPlans(plans);
const standardPlans = global.subPlans?.standard;
const standard = plans[0];
const standardConstants =
standard.currentSubLevel && standardPlans
? standardPlans[
standard.currentSubLevel === StandardSubLevelEnum.custom
? StandardSubLevelEnum.advanced
: standard.currentSubLevel
]
: undefined;
return {
[SubTypeEnum.standard]: standardConstants
? buildStandardPlan(standard, standardConstants)
: undefined
};
};
// 获取团队所有套餐内容
export const getTeamPlanStatus = async ({
teamId
}: {
teamId: string;
}): Promise<TeamPlanStatusType> => {
/** 配置里的套餐 */
const standardPlans = global.subPlans?.standard;
/* Get all plans and datasetSize */
@@ -190,6 +205,7 @@ export const getTeamPlanStatus = async ({
const teamStandardPlans = sortStandPlans(
plans.filter((plan) => plan.type === SubTypeEnum.standard)
);
/** 数据库里的,用户目前 active 的套餐 */
const standardPlan = teamStandardPlans[0];
const extraDatasetSize = plans.filter((plan) => plan.type === SubTypeEnum.extraDatasetSize);
@@ -230,6 +246,7 @@ export const getTeamPlanStatus = async ({
standardMaxDatasetSize +
extraDatasetSize.reduce((acc, cur) => acc + (cur.currentExtraDatasetSize || 0), 0);
/** 静态的套餐配置,如果是 custom 则返回 advanced */
const standardConstants =
standardPlan?.currentSubLevel && standardPlans
? standardPlans[
@@ -242,56 +259,8 @@ export const getTeamPlanStatus = async ({
teamPoint.updateTeamPointsCache({ teamId, totalPoints, surplusPoints });
return {
standard:
standardPlan.currentSubLevel === StandardSubLevelEnum.custom && standardConstants
? {
...standardPlan,
maxTeamMember: standardPlan?.maxTeamMember ?? standardConstants.maxTeamMember,
maxApp: standardPlan?.maxApp ?? standardConstants.maxAppAmount,
maxDataset: standardPlan?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute:
standardPlan?.requestsPerMinute ?? standardConstants.requestsPerMinute,
chatHistoryStoreDuration:
standardPlan?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
maxDatasetSize: standardPlan?.maxDatasetSize ?? standardConstants.maxDatasetSize,
websiteSyncPerDataset:
standardPlan?.websiteSyncPerDataset || standardConstants.websiteSyncPerDataset,
appRegistrationCount:
standardPlan?.appRegistrationCount ?? standardConstants.appRegistrationCount,
auditLogStoreDuration:
standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
ticketResponseTime:
standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime,
customDomain: standardPlan?.customDomain ?? standardConstants.customDomain,
maxUploadFileSize:
standardPlan?.maxUploadFileSize ?? standardConstants.maxUploadFileSize,
maxUploadFileCount:
standardPlan?.maxUploadFileCount ?? standardConstants.maxUploadFileCount
}
: standardPlan,
standardConstants: standardConstants
? {
...standardConstants,
maxTeamMember: standardPlan?.maxTeamMember ?? standardConstants.maxTeamMember,
maxAppAmount: standardPlan?.maxApp ?? standardConstants.maxAppAmount,
maxDatasetAmount: standardPlan?.maxDataset ?? standardConstants.maxDatasetAmount,
requestsPerMinute: standardPlan?.requestsPerMinute ?? standardConstants.requestsPerMinute,
chatHistoryStoreDuration:
standardPlan?.chatHistoryStoreDuration ?? standardConstants.chatHistoryStoreDuration,
maxDatasetSize: standardPlan?.maxDatasetSize ?? standardConstants.maxDatasetSize,
websiteSyncPerDataset:
standardPlan?.websiteSyncPerDataset || standardConstants.websiteSyncPerDataset,
appRegistrationCount:
standardPlan?.appRegistrationCount ?? standardConstants.appRegistrationCount,
auditLogStoreDuration:
standardPlan?.auditLogStoreDuration ?? standardConstants.auditLogStoreDuration,
ticketResponseTime:
standardPlan?.ticketResponseTime ?? standardConstants.ticketResponseTime,
customDomain: standardPlan?.customDomain ?? standardConstants.customDomain,
maxUploadFileSize: standardPlan?.maxUploadFileSize ?? standardConstants.maxUploadFileSize,
maxUploadFileCount:
standardPlan?.maxUploadFileCount ?? standardConstants.maxUploadFileCount
}
[SubTypeEnum.standard]: standardConstants
? buildStandardPlan(standardPlan, standardConstants)
: undefined,
totalPoints,
@@ -301,6 +270,7 @@ export const getTeamPlanStatus = async ({
};
};
/* ===== Buffer controller ===== */
export const teamPoint = {
getTeamPoints: async ({ teamId }: { teamId: string }) => {
const surplusCacheKey = `${CacheKeyEnum.team_point_surplus}:${teamId}`;
@@ -368,9 +338,7 @@ export const teamQPM = {
// 2. Computed
const teamPlanStatus = await getTeamPlanStatus({ teamId });
const limit =
teamPlanStatus[SubTypeEnum.standard]?.requestsPerMinute ??
teamPlanStatus.standardConstants?.requestsPerMinute;
const limit = teamPlanStatus[SubTypeEnum.standard]?.requestsPerMinute;
if (!limit) {
if (process.env.CHAT_MAX_QPM) return Number(process.env.CHAT_MAX_QPM);