Skip to content
1 change: 0 additions & 1 deletion document/content/self-host/config/env.en.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,6 @@ These variables are mainly read by `pro/admin`. Admin also uses the shared App/A
| `BING_OAUTH_CLIENT_SECRET` | Empty | Bing OAuth client secret. |
| `BING_OAUTH_REFRESH_TOKEN` | Empty | Bing OAuth refresh token. |
| `SHOW_WECOM_CONFIG` | `false` | Whether WeCom configuration is shown. |
| `WECOM_DEV` | `false` | Development mode switch for WeCom Pay. |

## Code Sandbox Variables

Expand Down
1 change: 0 additions & 1 deletion document/content/self-host/config/env.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,6 @@ DOC2X_KEY=your-api-key
| `BING_OAUTH_CLIENT_SECRET` | 空 | Bing OAuth Client Secret。 |
| `BING_OAUTH_REFRESH_TOKEN` | 空 | Bing OAuth Refresh Token。 |
| `SHOW_WECOM_CONFIG` | `false` | 是否展示企业微信相关配置。 |
| `WECOM_DEV` | `false` | 企业微信支付相关开发模式开关。 |

## Code Sandbox 变量

Expand Down
7 changes: 2 additions & 5 deletions packages/global/support/user/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ export const UserTagsSchema = z.enum(['wecom']);
export const UserTagsEnum = UserTagsSchema.enum;
export type UserTagsType = z.infer<typeof UserTagsSchema>;

export type UserMetaType = {
isActivatedWecomLicense?: boolean;
};
export type UserMetaType = Record<string, unknown>;

export type UserModelSchema = {
_id: string;
Expand Down Expand Up @@ -60,8 +58,7 @@ export const TeamMetaSchema = z.object({
wecom: z
.object({
permanentCode: z.string(),
corpId: z.string(),
licenseCapacity: z.int().min(0).default(0)
corpId: z.string()
})
.optional()
});
Expand Down
16 changes: 1 addition & 15 deletions packages/global/support/wallet/sub/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,6 @@ export const TeamStandardSubPlanItemSchema = z.object({
// Active
annualBonusPoints: z.int().optional(), // 年度赠送积分

/** 企微设置 */
wecom: z
.object({
price: z.number().describe('企微价格'),
points: z.number().describe('企微积分')
})
.nullish(),

/** @deprecated */
pointPrice: z.number().optional()
});
Expand Down Expand Up @@ -103,13 +95,7 @@ const TeamStandardSubPlanItemInputSchema = TeamStandardSubPlanItemSchema.extend(
maxUploadFileSize: configOptionalIntegerInputSchema,
maxUploadFileCount: configOptionalIntegerInputSchema,
annualBonusPoints: configOptionalIntegerInputSchema,
pointPrice: configOptionalNumberInputSchema,
wecom: z
.object({
price: NumSchema.nonnegative(),
points: NumSchema.nonnegative()
})
.nullish()
pointPrice: configOptionalNumberInputSchema
});

const CustomStandardSubPlanItemInputSchema = z.preprocess((value) => {
Expand Down
9 changes: 9 additions & 0 deletions packages/service/support/user/team/teamMemberSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ defineIndex(TeamMemberSchema, {
key: { userId: 1, teamId: 1 },
options: { unique: true, background: true }
});
defineIndex(TeamMemberSchema, {
key: { teamId: 1, wecomUserId: 1 },
options: {
unique: true,
partialFilterExpression: { wecomUserId: { $exists: true } },
background: true
},
deprecated: true
});

defineIndex(TeamMemberSchema, {
key: { userId: 1 },
Expand Down
55 changes: 12 additions & 43 deletions packages/service/support/wallet/sub/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
} from '@fastgpt/global/support/wallet/sub/type';
import dayjs from 'dayjs';
import { type ClientSession } from '../../../common/mongo';
import { addMonths, addDays } from 'date-fns';
import { addMonths } from 'date-fns';
import { readFromSecondary } from '../../../common/mongo/utils';
import { TeamPointCache, teamQpmCache } from '@fastgpt/dal/redis/caches';
import { getLogger, LogCategories } from '../../../common/logger';
Expand Down Expand Up @@ -65,7 +65,6 @@ export const buildStandardPlan = (
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,
Expand All @@ -85,44 +84,19 @@ export const buildStandardPlan = (

export const initTeamFreePlan = async ({
teamId,
isWecomTeam = false,
session
}: {
teamId: string;
isWecomTeam?: boolean;
session?: ClientSession;
}) => {
const freePoints = isWecomTeam
? Math.round((global.subPlans?.standard?.basic?.totalPoints ?? 4000) / 2)
: global?.subPlans?.standard?.[StandardSubLevelEnum.free]?.totalPoints || 100;
const freePoints = global?.subPlans?.standard?.[StandardSubLevelEnum.free]?.totalPoints || 100;

const freePlan = await MongoTeamSub.findOne({
teamId,
type: SubTypeEnum.standard,
currentSubLevel: StandardSubLevelEnum.free
});

// Get basic plan config for wecom mode
const specialConfig: Record<string, any> | null = (() => {
const config = global?.subPlans?.standard?.[StandardSubLevelEnum.basic];
if (isWecomTeam && config) {
return {
maxTeamMember: config.maxTeamMember,
maxApp: config.maxAppAmount,
maxDataset: config.maxDatasetAmount,
requestsPerMinute: config.requestsPerMinute,
chatHistoryStoreDuration: config.chatHistoryStoreDuration,
maxDatasetSize: config.maxDatasetSize,
websiteSyncPerDataset: config.websiteSyncPerDataset,
appRegistrationCount: config.appRegistrationCount,
auditLogStoreDuration: config.auditLogStoreDuration,
ticketResponseTime: config.ticketResponseTime,
customDomain: config.customDomain
} as TeamSubSchemaType;
}
return null;
})();

// Reset one month free plan
if (freePlan) {
freePlan.currentMode = SubModeEnum.month;
Expand All @@ -139,13 +113,6 @@ export const initTeamFreePlan = async ({
? freePlan.surplusPoints + freePoints
: freePoints;

// Apply basic plan config for wecom, but with limited points and dataset size
if (specialConfig) {
for (const key in specialConfig) {
(freePlan as any)[key] = specialConfig[key];
}
}

return freePlan.save({ session });
}

Expand All @@ -157,32 +124,34 @@ export const initTeamFreePlan = async ({
currentMode: SubModeEnum.month,
nextMode: SubModeEnum.month,
startTime: new Date(),
expiredTime: isWecomTeam ? addDays(new Date(), 15) : addMonths(new Date(), 1),
expiredTime: addMonths(new Date(), 1),

currentSubLevel: StandardSubLevelEnum.free,
nextSubLevel: StandardSubLevelEnum.free,

totalPoints: freePoints,
surplusPoints: freePoints,
...(specialConfig && specialConfig)
surplusPoints: freePoints
}
],
{ session, ordered: true }
);
};

// 获取团队标准套餐
export const getTeamStandPlan = async ({ teamId }: { teamId: string }) => {
const standardPlans = global.subPlans?.standard;
export const getTeamStandPlan = async ({
teamId,
readFromPrimary = false
}: {
teamId: string;
readFromPrimary?: boolean;
}) => {
const plans = await MongoTeamSub.find(
{
teamId,
type: SubTypeEnum.standard
},
undefined,
{
...readFromSecondary
}
readFromPrimary ? { readPreference: 'primary' } : { ...readFromSecondary }
).lean();
sortStandPlans(plans);

Expand Down
40 changes: 40 additions & 0 deletions packages/service/test/common/mongo/indexManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,46 @@ describe('MongoIndexManager.syncModelIndexes', () => {
});
});

it('removes deprecated partial compound indexes', async () => {
const schema = new Schema(
{
teamId: String,
wecomUserId: String
},
{ autoIndex: false }
);
defineDeprecatedTestIndexes(schema, [
{
indexName: 'teamId_1_wecomUserId_1',
key: { teamId: 1, wecomUserId: 1 },
options: {
unique: true,
partialFilterExpression: { wecomUserId: { $exists: true } }
}
}
]);
const model = createModel({ schema });
await model.collection.createIndex(
{ teamId: 1, wecomUserId: 1 },
{
name: 'teamId_1_wecomUserId_1',
unique: true,
partialFilterExpression: { wecomUserId: { $exists: true } }
}
);

const result = await MongoIndexManager.syncModelIndexes({ model, logger });

expect(await getIndexNames(model)).not.toContain('teamId_1_wecomUserId_1');
expect(result.cleanupReport.items).toEqual([
expect.objectContaining({
action: 'drop',
applied: true,
indexName: 'teamId_1_wecomUserId_1'
})
]);
});

it('does not delete schema-external indexes when the Schema has no deprecated declarations', async () => {
const schema = new Schema(
{ currentField: String, customerField: String },
Expand Down
24 changes: 24 additions & 0 deletions packages/service/test/support/user/team/teamMemberSchema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { getDeprecatedIndexes } from '@fastgpt/service/common/mongo/schemaIndexes';

describe('TeamMember schema', () => {
it('does not declare a duplicated WeCom user identity or active index', () => {
expect(MongoTeamMember.schema.path('wecomUserId')).toBeUndefined();
expect(MongoTeamMember.schema.indexes()).not.toContainEqual([
{ teamId: 1, wecomUserId: 1 },
expect.any(Object)
]);
});

it('registers the removed WeCom index for safe cleanup', () => {
expect(getDeprecatedIndexes(MongoTeamMember.schema)).toContainEqual({
indexName: 'teamId_1_wecomUserId_1',
key: { teamId: 1, wecomUserId: 1 },
options: {
unique: true,
partialFilterExpression: { wecomUserId: { $exists: true } }
}
});
});
});
32 changes: 22 additions & 10 deletions packages/service/test/support/wallet/sub/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '@fastgpt/service/support/wallet/sub/utils';
import { MongoTeamSub } from '@fastgpt/service/support/wallet/sub/schema';
import { redisCacheAdapter } from '@fastgpt/dal/redis/adapter';
import { addMonths } from 'date-fns';

// Valid ObjectId for testing
const mockTeamId = '507f1f77bcf86cd799439011';
Expand Down Expand Up @@ -444,21 +445,11 @@ describe('buildStandardPlan', () => {
expect(result.customDescriptions).toEqual(['特性A', '特性B']);
});

it('wecom 取自 standardConstants', () => {
const constants: TeamStandardSubPlanItemType = {
...baseConstants,
wecom: { price: 9.9, points: 500 }
};
const result = buildStandardPlan(baseStandard, constants);
expect(result.wecom).toEqual({ price: 9.9, points: 500 });
});

it('standardConstants 展示字段为 undefined 时结果也为 undefined', () => {
const result = buildStandardPlan(baseStandard, baseConstants);
expect(result.priceDescription).toBeUndefined();
expect(result.customFormUrl).toBeUndefined();
expect(result.customDescriptions).toBeUndefined();
expect(result.wecom).toBeUndefined();
});
});

Expand Down Expand Up @@ -691,6 +682,9 @@ describe('initTeamFreePlan', () => {

expect(MongoTeamSub.findOne).toHaveBeenCalled();
expect(MongoTeamSub.create).toHaveBeenCalled();
const [createdPlan] = (MongoTeamSub.create as any).mock.calls[0][0];
expect(createdPlan.expiredTime).toEqual(addMonths(createdPlan.startTime, 1));
expect(createdPlan.maxTeamMember).toBeUndefined();
expect(result).toEqual([mockCreatedPlan]);
});

Expand Down Expand Up @@ -790,6 +784,24 @@ describe('getTeamStandPlan', () => {
expect(result[SubTypeEnum.standard]?.name).toBe('Basic Plan');
});

it('支持强制从主库读取团队标准套餐', async () => {
const mockQuery = {
lean: vi.fn().mockResolvedValue([])
};
vi.spyOn(MongoTeamSub, 'find').mockReturnValue(mockQuery as any);

await getTeamStandPlan({ teamId: mockTeamId, readFromPrimary: true });

expect(MongoTeamSub.find).toHaveBeenCalledWith(
{
teamId: mockTeamId,
type: SubTypeEnum.standard
},
undefined,
{ readPreference: 'primary' }
);
});

it('开启标准套餐配置但没有套餐记录时返回 undefined', async () => {
vi.spyOn(MongoTeamSub, 'find').mockReturnValue({
lean: vi.fn().mockResolvedValue([])
Expand Down
2 changes: 1 addition & 1 deletion pro
Submodule pro updated from 8eedeb to b8cb22
9 changes: 1 addition & 8 deletions projects/app/src/components/CommunityModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import { useClientTranslation } from '@fastgpt/web/i18n/useClientTranslation';
import Markdown from '../Markdown';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useUserStore } from '@/web/support/user/useUserStore';

const CommunityModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useClientTranslation();
const { feConfigs } = useSystemStore();
const { userInfo } = useUserStore();
const isWecomTeam = !!userInfo?.team.isWecomTeam;

return (
<MyModal
Expand All @@ -20,11 +17,7 @@ const CommunityModal = ({ onClose }: { onClose: () => void }) => {
title={t('common:system.Concat us')}
>
<ModalBody textAlign={'center'}>
{isWecomTeam ? (
t('common:contact_email', { email: 'archer@fastgpt.io' })
) : (
<Markdown source={feConfigs?.concatMd || ''} />
)}
<Markdown source={feConfigs?.concatMd || ''} />
</ModalBody>

<ModalFooter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const EnterpriseAuthNoticeModal = () => {
router.pathname === '/dashboard/agent' &&
!!feConfigs?.show_enterprise_auth &&
!!teamId &&
!userInfo?.team?.isWecomTeam &&
canCheckEnterpriseAuthNotice &&
!isAccountCancellationPending &&
!enterpriseAuthNoticeReadTeamIds?.includes(teamId);
Expand Down
Loading
Loading