diff --git a/packages/global/common/error/code/team.ts b/packages/global/common/error/code/team.ts index 592d7995a1f6..92ee7ad6eaea 100644 --- a/packages/global/common/error/code/team.ts +++ b/packages/global/common/error/code/team.ts @@ -1,3 +1,4 @@ +import { EnterpriseAuthErrEnum } from '../../../support/user/team/enterpriseAuth/constant'; import { i18nT } from '../../i18n/utils'; import type { ErrType } from '../errorCode'; /* team: 500000 */ @@ -154,17 +155,76 @@ const teamErr = [ { statusText: TeamErrEnum.sandboxNotSupport, message: i18nT('common:code_error.team_error.sandbox_not_support') + }, + { + statusText: EnterpriseAuthErrEnum.disabled, + message: i18nT('common:enterprise_auth.error.disabled') + }, + { + statusText: EnterpriseAuthErrEnum.serviceNotConfigured, + message: i18nT('common:enterprise_auth.error.service_not_configured') + }, + { + statusText: EnterpriseAuthErrEnum.noRemainingTimes, + message: i18nT('common:enterprise_auth.error.no_remaining_times') + }, + { + statusText: EnterpriseAuthErrEnum.alreadyVerified, + message: i18nT('common:enterprise_auth.error.already_verified') + }, + { + statusText: EnterpriseAuthErrEnum.enterpriseOccupied, + message: i18nT('common:enterprise_auth.error.enterprise_occupied') + }, + { + statusText: EnterpriseAuthErrEnum.tooFrequent, + message: i18nT('common:enterprise_auth.error.too_frequent') + }, + { + statusText: EnterpriseAuthErrEnum.serviceError, + message: i18nT('common:enterprise_auth.error.service_error') + }, + { + statusText: EnterpriseAuthErrEnum.serviceTimeout, + message: i18nT('common:enterprise_auth.error.service_timeout') + }, + { + statusText: EnterpriseAuthErrEnum.infoFailed, + message: i18nT('common:enterprise_auth.error.info_failed') + }, + { + statusText: EnterpriseAuthErrEnum.taskNotFound, + message: i18nT('common:enterprise_auth.error.task_not_found') + }, + { + statusText: EnterpriseAuthErrEnum.taskExpired, + message: i18nT('common:enterprise_auth.error.task_expired') + }, + { + statusText: EnterpriseAuthErrEnum.amountError, + message: i18nT('common:enterprise_auth.error.amount_error') + }, + { + statusText: EnterpriseAuthErrEnum.amountFailed, + message: i18nT('common:enterprise_auth.error.amount_failed') + }, + { + statusText: EnterpriseAuthErrEnum.processing, + message: i18nT('common:enterprise_auth.error.processing') } ]; -export default teamErr.reduce((acc, cur, index) => { - return { - ...acc, - [cur.statusText]: { - code: 500000 + index, - statusText: cur.statusText, - message: cur.message, - data: null - } - }; -}, {} as ErrType<`${TeamErrEnum}`>); +export default teamErr.reduce( + (acc, cur, index) => { + return { + ...acc, + [cur.statusText]: { + code: 500000 + index, + statusText: cur.statusText, + message: cur.message, + data: null + } + }; + }, + {} as ErrType<`${TeamErrEnum}` | `${EnterpriseAuthErrEnum}`> +); diff --git a/packages/global/common/middle/tracks/constants.ts b/packages/global/common/middle/tracks/constants.ts index 9eeed6f72961..cf2394f2bc82 100644 --- a/packages/global/common/middle/tracks/constants.ts +++ b/packages/global/common/middle/tracks/constants.ts @@ -11,6 +11,8 @@ export enum TrackEnum { clickOperationalAd = 'clickOperationalAd', closeOperationalAd = 'closeOperationalAd', teamChatQPM = 'teamChatQPM', + enterpriseAuthStart = 'enterpriseAuthStart', + enterpriseAuthBenefitGrant = 'enterpriseAuthBenefitGrant', // Admin cron job tracks subscriptionDeleted = 'subscriptionDeleted', diff --git a/packages/global/common/system/types/index.ts b/packages/global/common/system/types/index.ts index 952143b76131..0bcfa3af56f3 100644 --- a/packages/global/common/system/types/index.ts +++ b/packages/global/common/system/types/index.ts @@ -65,6 +65,7 @@ export type FastGPTFeConfigsType = { show_aiproxy?: boolean; show_coupon?: boolean; show_discount_coupon?: boolean; + show_enterprise_auth?: boolean; showWecomConfig?: boolean; show_dataset_feishu?: boolean; diff --git a/packages/global/openapi/support/user/team/enterpriseAuth/api.ts b/packages/global/openapi/support/user/team/enterpriseAuth/api.ts new file mode 100644 index 000000000000..93cf81411ffe --- /dev/null +++ b/packages/global/openapi/support/user/team/enterpriseAuth/api.ts @@ -0,0 +1,207 @@ +import z from 'zod'; +import { + EnterpriseAuthAmountMaxErrorTimes, + EnterpriseAuthMaxTimes, + TeamEnterpriseAuthStatusSchema, + TeamEnterpriseAuthTaskStatusSchema +} from '../../../../../support/user/team/enterpriseAuth/constant'; +import { + isBankAccount, + isUnifiedCreditCode, + normalizeBankAccount, + normalizeUnifiedCreditCode +} from '../../../../../support/user/team/enterpriseAuth/utils'; + +/* ============================================================================ + * API: 获取企业认证状态 + * Route: GET /api/proApi/support/user/team/enterpriseAuth/status + * Method: GET + * Description: 获取当前团队企业认证入口开关、认证状态和可恢复任务信息 + * Tags: ['团队管理'] + * ============================================================================ */ + +export const EnterpriseAuthLightTaskSchema = z.object({ + taskId: z.string().meta({ description: '认证任务 ID' }), + status: TeamEnterpriseAuthTaskStatusSchema.meta({ description: '当前任务状态' }), + amountErrorTimes: z.number().int().meta({ + description: '当前任务金额填写错误次数', + example: 0 + }) +}); + +export const GetEnterpriseAuthStatusResponseSchema = z.object({ + enabled: z.boolean().meta({ description: '企业认证入口是否开启' }), + status: TeamEnterpriseAuthStatusSchema.optional().meta({ description: '团队认证状态' }), + usedTimes: z + .number() + .int() + .optional() + .meta({ description: `已使用认证次数,最多 ${EnterpriseAuthMaxTimes} 次`, example: 0 }), + canManage: z.boolean().optional().meta({ description: '当前成员是否可管理企业认证' }), + verifiedEnterpriseName: z.string().optional().meta({ description: '认证通过企业名称' }), + currentTask: EnterpriseAuthLightTaskSchema.optional().meta({ + description: '未完成认证任务' + }), + lastErrorCode: z.string().optional().meta({ description: '最近一次失败错误码' }), + lastErrorMessage: z.string().optional().meta({ description: '最近一次失败提示' }) +}); +export type GetEnterpriseAuthStatusResponseType = z.infer< + typeof GetEnterpriseAuthStatusResponseSchema +>; + +/* ============================================================================ + * API: 获取当前企业认证任务详情 + * Route: GET /api/proApi/support/user/team/enterpriseAuth/currentTaskDetail + * Method: GET + * Description: 获取待金额验证任务的完整展示信息,不返回验证金额 + * Tags: ['团队管理'] + * ============================================================================ */ + +export const GetEnterpriseAuthCurrentTaskDetailResponseSchema = z.object({ + taskId: z.string().meta({ description: '认证任务 ID' }), + status: TeamEnterpriseAuthTaskStatusSchema.meta({ description: '当前任务状态' }), + enterpriseName: z.string().meta({ description: '企业名称' }), + unifiedCreditCode: z.string().meta({ description: '统一社会信用代码' }), + legalPersonName: z.string().meta({ description: '法人姓名' }), + bankName: z.string().meta({ description: '开户银行名称' }), + bankAccount: z.string().meta({ description: '企业银行账号,仅此接口返回完整值' }), + contactName: z.string().meta({ description: '联系人姓名' }), + contactTitle: z.string().meta({ description: '联系人职位' }), + contactPhone: z.string().meta({ description: '联系人手机号' }), + demand: z.string().meta({ description: '需求描述' }), + amountErrorTimes: z.number().int().meta({ description: '金额错误次数' }) +}); +export type GetEnterpriseAuthCurrentTaskDetailResponseType = z.infer< + typeof GetEnterpriseAuthCurrentTaskDetailResponseSchema +>; + +/* ============================================================================ + * API: 获取企业认证银行列表 + * Route: GET /api/proApi/support/user/team/enterpriseAuth/banks + * Method: GET + * Description: 从小额汇款服务获取银行编码到总行名称映射 + * Tags: ['团队管理'] + * ============================================================================ */ + +export const GetEnterpriseAuthBanksResponseSchema = z.record(z.string(), z.string()).meta({ + description: '银行简称到银行公司全称的映射' +}); +export type GetEnterpriseAuthBanksResponseType = z.infer< + typeof GetEnterpriseAuthBanksResponseSchema +>; + +const EnterpriseAuthRequiredStringSchema = z.string().trim().min(1); +const BankAccountSchema = EnterpriseAuthRequiredStringSchema.transform((account) => + normalizeBankAccount(account) +) + .pipe(z.string().refine(isBankAccount)) + .meta({ + description: '企业银行账号,15-19 位数字,可输入空格分隔', + example: '4111111111111111' + }); +const UnifiedCreditCodeSchema = EnterpriseAuthRequiredStringSchema.transform((code) => + normalizeUnifiedCreditCode(code) +) + .pipe(z.string().refine(isUnifiedCreditCode)) + .meta({ + description: + '统一社会信用代码,18 位大写数字或字母(不包含 I/O/S/V/Z),需通过 GB 32100-2015 校验码校验', + example: '91310000MA1K000006' + }); +const VerifyEnterpriseAuthAmountCentSchema = z.number().int().positive(); + +export const StartEnterpriseAuthBodySchema = z.object({ + enterpriseName: EnterpriseAuthRequiredStringSchema.max(100).meta({ + description: '企业全称,需与银行开户名一致', + example: '示例科技有限公司' + }), + unifiedCreditCode: UnifiedCreditCodeSchema, + legalPersonName: EnterpriseAuthRequiredStringSchema.max(50).meta({ + description: '法人姓名', + example: '张三' + }), + bankAccount: BankAccountSchema, + bankName: EnterpriseAuthRequiredStringSchema.max(80).meta({ + description: '开户银行名称', + example: '中国工商银行' + }), + contactName: EnterpriseAuthRequiredStringSchema.max(50).meta({ + description: '联系人姓名', + example: '李四' + }), + contactTitle: EnterpriseAuthRequiredStringSchema.max(50).meta({ + description: '联系人职位', + example: '产品负责人' + }), + contactPhone: EnterpriseAuthRequiredStringSchema.max(30).meta({ + description: '联系人手机号', + example: '13800000000' + }), + demand: EnterpriseAuthRequiredStringSchema.max(500).meta({ + description: '使用需求描述', + example: '希望了解企业知识库和工作流能力' + }) +}); +export type StartEnterpriseAuthBodyType = z.infer; + +/* ============================================================================ + * API: 发起企业认证 + * Route: POST /api/proApi/support/user/team/enterpriseAuth/start + * Method: POST + * Description: 创建小额汇款认证任务,打款成功后返回待金额验证任务 + * Tags: ['团队管理'] + * ============================================================================ */ + +export const StartEnterpriseAuthResponseSchema = z.object({ + status: TeamEnterpriseAuthStatusSchema.meta({ description: '团队认证状态' }), + currentTask: EnterpriseAuthLightTaskSchema.optional().meta({ + description: '未完成认证任务;仅 pending_amount/amount_failed 可进入金额验证页' + }), + usedTimes: z + .number() + .int() + .meta({ description: `已使用认证次数,最多 ${EnterpriseAuthMaxTimes} 次` }), + message: z.string().optional().meta({ description: '流程提示' }) +}); +export type StartEnterpriseAuthResponseType = z.infer; + +export const VerifyEnterpriseAuthAmountBodySchema = z.object({ + taskId: z.string().min(1).meta({ description: '认证任务 ID' }), + amountCent: VerifyEnterpriseAuthAmountCentSchema.meta({ + description: '用户填写的到账金额,单位为分', + example: 123 + }) +}); +export type VerifyEnterpriseAuthAmountBodyType = z.infer< + typeof VerifyEnterpriseAuthAmountBodySchema +>; + +/* ============================================================================ + * API: 验证企业认证打款金额 + * Route: POST /api/proApi/support/user/team/enterpriseAuth/verifyAmount + * Method: POST + * Description: 校验到账金额并在成功后发放企业认证赠送权益 + * Tags: ['团队管理'] + * ============================================================================ */ + +export const VerifyEnterpriseAuthAmountResponseSchema = z.object({ + status: TeamEnterpriseAuthStatusSchema.meta({ description: '团队认证状态' }), + verifiedEnterpriseName: z.string().optional().meta({ description: '认证通过企业名称' }), + amountMaxErrorTimes: z.literal(EnterpriseAuthAmountMaxErrorTimes).meta({ + description: '金额最大错误次数' + }) +}); +export type VerifyEnterpriseAuthAmountResponseType = z.infer< + typeof VerifyEnterpriseAuthAmountResponseSchema +>; + +/* ============================================================================ + * API: 重置企业认证待金额验证任务 + * Route: POST /api/proApi/support/user/team/enterpriseAuth/reset + * Method: POST + * Description: 用户确认信息有误后取消当前待金额验证任务 + * Tags: ['团队管理'] + * ============================================================================ */ + +export const ResetEnterpriseAuthResponseSchema = z.undefined().meta({ description: '操作成功' }); +export type ResetEnterpriseAuthResponseType = z.infer; diff --git a/packages/global/openapi/support/user/team/enterpriseAuth/index.ts b/packages/global/openapi/support/user/team/enterpriseAuth/index.ts new file mode 100644 index 000000000000..777afb2089fe --- /dev/null +++ b/packages/global/openapi/support/user/team/enterpriseAuth/index.ts @@ -0,0 +1,125 @@ +import type { OpenAPIPath } from '../../../../type'; +import { DevApiTagsMap } from '../../../../tag'; +import { + GetEnterpriseAuthBanksResponseSchema, + GetEnterpriseAuthCurrentTaskDetailResponseSchema, + GetEnterpriseAuthStatusResponseSchema, + ResetEnterpriseAuthResponseSchema, + StartEnterpriseAuthBodySchema, + StartEnterpriseAuthResponseSchema, + VerifyEnterpriseAuthAmountBodySchema, + VerifyEnterpriseAuthAmountResponseSchema +} from './api'; + +export const EnterpriseAuthPath: OpenAPIPath = { + '/proApi/support/user/team/enterpriseAuth/status': { + get: { + summary: '获取企业认证状态', + tags: [DevApiTagsMap.teamManage], + responses: { + 200: { + description: '企业认证状态', + content: { + 'application/json': { + schema: GetEnterpriseAuthStatusResponseSchema + } + } + } + } + } + }, + '/proApi/support/user/team/enterpriseAuth/currentTaskDetail': { + get: { + summary: '获取当前企业认证任务详情', + tags: [DevApiTagsMap.teamManage], + responses: { + 200: { + description: '当前待金额验证任务详情', + content: { + 'application/json': { + schema: GetEnterpriseAuthCurrentTaskDetailResponseSchema + } + } + } + } + } + }, + '/proApi/support/user/team/enterpriseAuth/banks': { + get: { + summary: '获取企业认证银行列表', + tags: [DevApiTagsMap.teamManage], + responses: { + 200: { + description: '银行编码到总行名称映射', + content: { + 'application/json': { + schema: GetEnterpriseAuthBanksResponseSchema + } + } + } + } + } + }, + '/proApi/support/user/team/enterpriseAuth/start': { + post: { + summary: '发起企业认证', + tags: [DevApiTagsMap.teamManage], + requestBody: { + content: { + 'application/json': { + schema: StartEnterpriseAuthBodySchema + } + } + }, + responses: { + 200: { + description: '企业认证任务状态', + content: { + 'application/json': { + schema: StartEnterpriseAuthResponseSchema + } + } + } + } + } + }, + '/proApi/support/user/team/enterpriseAuth/verifyAmount': { + post: { + summary: '验证企业认证打款金额', + tags: [DevApiTagsMap.teamManage], + requestBody: { + content: { + 'application/json': { + schema: VerifyEnterpriseAuthAmountBodySchema + } + } + }, + responses: { + 200: { + description: '金额验证结果', + content: { + 'application/json': { + schema: VerifyEnterpriseAuthAmountResponseSchema + } + } + } + } + } + }, + '/proApi/support/user/team/enterpriseAuth/reset': { + post: { + summary: '取消当前企业认证任务并重新填写', + tags: [DevApiTagsMap.teamManage], + responses: { + 200: { + description: '取消成功', + content: { + 'application/json': { + schema: ResetEnterpriseAuthResponseSchema + } + } + } + } + } + } +}; diff --git a/packages/global/openapi/support/user/team/index.ts b/packages/global/openapi/support/user/team/index.ts index 0080fab3a88f..484123dd190f 100644 --- a/packages/global/openapi/support/user/team/index.ts +++ b/packages/global/openapi/support/user/team/index.ts @@ -1,8 +1,10 @@ import type { OpenAPIPath } from '../../../type'; import { DevApiTagsMap } from '../../../tag'; import { UpdateTeamBodySchema } from './api'; +import { EnterpriseAuthPath } from './enterpriseAuth'; export const TeamPath: OpenAPIPath = { + ...EnterpriseAuthPath, '/api/support/user/team/update': { post: { summary: '更新团队信息', diff --git a/packages/global/support/user/audit/constants.ts b/packages/global/support/user/audit/constants.ts index a03c3b4a3ac4..f48ac0cd6776 100644 --- a/packages/global/support/user/audit/constants.ts +++ b/packages/global/support/user/audit/constants.ts @@ -91,6 +91,9 @@ export enum AuditEventEnum { EXPORT_BILL_RECORDS = 'EXPORT_BILL_RECORDS', CREATE_INVOICE = 'CREATE_INVOICE', SET_INVOICE_HEADER = 'SET_INVOICE_HEADER', + START_ENTERPRISE_AUTH = 'START_ENTERPRISE_AUTH', + VERIFY_ENTERPRISE_AUTH_AMOUNT = 'VERIFY_ENTERPRISE_AUTH_AMOUNT', + RESET_ENTERPRISE_AUTH_TASK = 'RESET_ENTERPRISE_AUTH_TASK', CREATE_API_KEY = 'CREATE_API_KEY', UPDATE_API_KEY = 'UPDATE_API_KEY', COPY_API_KEY = 'COPY_API_KEY', diff --git a/packages/global/support/user/team/enterpriseAuth/constant.ts b/packages/global/support/user/team/enterpriseAuth/constant.ts new file mode 100644 index 000000000000..451ae166ad1c --- /dev/null +++ b/packages/global/support/user/team/enterpriseAuth/constant.ts @@ -0,0 +1,75 @@ +import z from 'zod'; + +export const EnterpriseAuthMaxTimes = 3; +export const EnterpriseAuthTrialDays = 15; +export const EnterpriseAuthTaskExpireHours = 24; +export const EnterpriseAuthAmountMaxErrorTimes = 3; + +export const TeamEnterpriseAuthStatusSchema = z.enum([ + 'unverified', + 'verifying', + 'verified', + 'failed' +]); +export const TeamEnterpriseAuthStatusEnum = TeamEnterpriseAuthStatusSchema.enum; +export type TeamEnterpriseAuthStatusEnum = z.infer; + +export const TeamEnterpriseAuthTaskStatusSchema = z.enum([ + 'starting', + 'info_failed', + 'pending_amount', + 'amount_failed', + /** + * 事务内临时态:仅用于金额验证成功后防止并发重复发放权益。 + * 不允许作为长期业务状态存在,也不参与 pending 任务恢复或统一社会信用代码锁。 + */ + 'granting', + 'canceled', + 'expired', + 'failed', + 'verified', + 'service_failed' +]); +export const TeamEnterpriseAuthTaskStatusEnum = TeamEnterpriseAuthTaskStatusSchema.enum; +export type TeamEnterpriseAuthTaskStatusEnum = z.infer; + +export const EnterpriseAuthPendingTaskStatuses = [ + TeamEnterpriseAuthTaskStatusEnum.starting, + TeamEnterpriseAuthTaskStatusEnum.pending_amount, + TeamEnterpriseAuthTaskStatusEnum.amount_failed +] as const; + +const EnterpriseAuthErrValueSchema = z.enum([ + 'enterpriseAuthDisabled', + 'enterpriseAuthServiceNotConfigured', + 'enterpriseAuthNoRemainingTimes', + 'enterpriseAuthAlreadyVerified', + 'enterpriseAuthEnterpriseOccupied', + 'enterpriseAuthTooFrequent', + 'enterpriseAuthServiceError', + 'enterpriseAuthServiceTimeout', + 'enterpriseAuthInfoFailed', + 'enterpriseAuthTaskNotFound', + 'enterpriseAuthTaskExpired', + 'enterpriseAuthAmountError', + 'enterpriseAuthAmountFailed', + 'enterpriseAuthProcessing' +]); +export const EnterpriseAuthErrEnum = { + disabled: EnterpriseAuthErrValueSchema.enum.enterpriseAuthDisabled, + serviceNotConfigured: EnterpriseAuthErrValueSchema.enum.enterpriseAuthServiceNotConfigured, + noRemainingTimes: EnterpriseAuthErrValueSchema.enum.enterpriseAuthNoRemainingTimes, + alreadyVerified: EnterpriseAuthErrValueSchema.enum.enterpriseAuthAlreadyVerified, + enterpriseOccupied: EnterpriseAuthErrValueSchema.enum.enterpriseAuthEnterpriseOccupied, + tooFrequent: EnterpriseAuthErrValueSchema.enum.enterpriseAuthTooFrequent, + serviceError: EnterpriseAuthErrValueSchema.enum.enterpriseAuthServiceError, + serviceTimeout: EnterpriseAuthErrValueSchema.enum.enterpriseAuthServiceTimeout, + infoFailed: EnterpriseAuthErrValueSchema.enum.enterpriseAuthInfoFailed, + taskNotFound: EnterpriseAuthErrValueSchema.enum.enterpriseAuthTaskNotFound, + taskExpired: EnterpriseAuthErrValueSchema.enum.enterpriseAuthTaskExpired, + amountError: EnterpriseAuthErrValueSchema.enum.enterpriseAuthAmountError, + amountFailed: EnterpriseAuthErrValueSchema.enum.enterpriseAuthAmountFailed, + processing: EnterpriseAuthErrValueSchema.enum.enterpriseAuthProcessing +} as const; +export const EnterpriseAuthErrSchema = z.enum(EnterpriseAuthErrEnum); +export type EnterpriseAuthErrEnum = z.infer; diff --git a/packages/global/support/user/team/enterpriseAuth/type.ts b/packages/global/support/user/team/enterpriseAuth/type.ts new file mode 100644 index 000000000000..931b7886cec3 --- /dev/null +++ b/packages/global/support/user/team/enterpriseAuth/type.ts @@ -0,0 +1,14 @@ +import z from 'zod'; + +export const EnterpriseAuthInfoSchema = z.object({ + enterpriseName: z.string(), + unifiedCreditCode: z.string(), + legalPersonName: z.string(), + bankName: z.string(), + bankAccount: z.string(), + contactName: z.string(), + contactTitle: z.string(), + contactPhone: z.string(), + demand: z.string() +}); +export type EnterpriseAuthInfoType = z.infer; diff --git a/packages/global/support/user/team/enterpriseAuth/utils.ts b/packages/global/support/user/team/enterpriseAuth/utils.ts new file mode 100644 index 000000000000..457fa7378804 --- /dev/null +++ b/packages/global/support/user/team/enterpriseAuth/utils.ts @@ -0,0 +1,38 @@ +const UnifiedCreditCodeChars = '0123456789ABCDEFGHJKLMNPQRTUWXY'; +const UnifiedCreditCodeWeights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28]; +const UnifiedCreditCodePattern = /^[0-9A-HJ-NP-RTUWXY]{18}$/; +const BankAccountPattern = /^\d{15,19}$/; + +export const normalizeUnifiedCreditCode = (code: string) => code.trim().toUpperCase(); + +export const normalizeBankAccount = (account: string) => account.replace(/\s+/g, ''); + +/** + * 按 GB 32100-2015 校验统一社会信用代码第 18 位校验码。 + */ +export const isUnifiedCreditCode = (code: string) => { + const normalizedCode = normalizeUnifiedCreditCode(code); + + if (!UnifiedCreditCodePattern.test(normalizedCode)) return false; + + const sum = UnifiedCreditCodeWeights.reduce((total, weight, index) => { + const value = UnifiedCreditCodeChars.indexOf(normalizedCode[index]); + return total + value * weight; + }, 0); + const checkValue = (() => { + const value = 31 - (sum % 31); + if (value === 31) return 0; + return value; + })(); + + return normalizedCode[17] === UnifiedCreditCodeChars[checkValue]; +}; + +/** + * 校验企业银行账号:去除空格后需为 15-19 位数字。 + */ +export const isBankAccount = (account: string) => { + const normalizedAccount = normalizeBankAccount(account); + + return BankAccountPattern.test(normalizedAccount); +}; diff --git a/packages/global/support/wallet/bill/constants.ts b/packages/global/support/wallet/bill/constants.ts index f3b7f1995545..60a598bff2ff 100644 --- a/packages/global/support/wallet/bill/constants.ts +++ b/packages/global/support/wallet/bill/constants.ts @@ -4,7 +4,8 @@ export enum BillTypeEnum { balance = 'balance', standSubPlan = 'standSubPlan', extraDatasetSub = 'extraDatasetSub', - extraPoints = 'extraPoints' + extraPoints = 'extraPoints', + activityGift = 'activityGift' } export const billTypeMap = { [BillTypeEnum.balance]: { @@ -18,6 +19,9 @@ export const billTypeMap = { }, [BillTypeEnum.extraPoints]: { label: i18nT('common:support.wallet.subscription.type.extraPoints') + }, + [BillTypeEnum.activityGift]: { + label: i18nT('common:support.wallet.bill.type.activityGift') } }; @@ -48,6 +52,7 @@ export enum BillPayWayEnum { alipay = 'alipay', bank = 'bank', coupon = 'coupon', + enterpriseAuth = 'enterpriseAuth', wecom = 'wecom' } @@ -67,6 +72,9 @@ export const billPayWayMap = { [BillPayWayEnum.coupon]: { label: i18nT('account_bill:payway_coupon') }, + [BillPayWayEnum.enterpriseAuth]: { + label: i18nT('common:support.wallet.bill.payWay.enterpriseAuth') + }, [BillPayWayEnum.wecom]: { label: i18nT('common:support.wallet.bill.payWay.wecom') } diff --git a/packages/global/support/wallet/bill/type.ts b/packages/global/support/wallet/bill/type.ts index 13be96850487..ba7a5679e302 100644 --- a/packages/global/support/wallet/bill/type.ts +++ b/packages/global/support/wallet/bill/type.ts @@ -26,7 +26,12 @@ export const BillSchema = z.object({ standSubLevel: z.enum(StandardSubLevelEnum).optional().meta({ description: '订阅等级' }), month: z.number().optional().meta({ description: '月数' }), datasetSize: z.number().optional().meta({ description: '数据集大小' }), - extraPoints: z.number().optional().meta({ description: '额外积分' }) + extraPoints: z.number().optional().meta({ description: '额外积分' }), + activitySource: z.literal('enterpriseAuth').optional().meta({ description: '活动赠送来源' }), + taskId: z.string().optional().meta({ description: '企业认证任务 ID' }), + durationDay: z.number().optional().meta({ description: '权益发放天数' }), + totalPoints: z.number().optional().meta({ description: '权益发放积分数' }), + grantedPlanCount: z.number().optional().meta({ description: '权益发放套餐数' }) }) .meta({ description: '元数据' }), refundData: z diff --git a/packages/global/test/openapi/support/user/team/enterpriseAuth/api.test.ts b/packages/global/test/openapi/support/user/team/enterpriseAuth/api.test.ts new file mode 100644 index 000000000000..bcc955855385 --- /dev/null +++ b/packages/global/test/openapi/support/user/team/enterpriseAuth/api.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; +import { + StartEnterpriseAuthBodySchema, + VerifyEnterpriseAuthAmountBodySchema +} from '../../../../../../openapi/support/user/team/enterpriseAuth/api'; + +describe('VerifyEnterpriseAuthAmountBodySchema', () => { + it('只接受严格正整数金额,避免无效金额消耗验证次数', () => { + expect( + VerifyEnterpriseAuthAmountBodySchema.parse({ + taskId: 'task-1', + amountCent: 123 + }) + ).toEqual({ + taskId: 'task-1', + amountCent: 123 + }); + + [0, -1, 1.5, '', '123', null, undefined, false].forEach((amountCent) => { + expect( + VerifyEnterpriseAuthAmountBodySchema.safeParse({ + taskId: 'task-1', + amountCent + }).success + ).toBe(false); + }); + }); +}); + +describe('StartEnterpriseAuthBodySchema', () => { + const validBody = { + enterpriseName: '示例科技有限公司', + unifiedCreditCode: '91310000MA1K000006', + legalPersonName: '张三', + bankAccount: '4111 1111 1111 1111', + bankName: '中国工商银行', + contactName: '李四', + contactTitle: '产品负责人', + contactPhone: '13800000000', + demand: '企业知识库试用' + }; + + it('按 GB 32100-2015 校验统一社会信用代码,并规范化大小写', () => { + expect( + StartEnterpriseAuthBodySchema.parse({ + ...validBody, + unifiedCreditCode: '91310000ma1k000006' + }).unifiedCreditCode + ).toBe('91310000MA1K000006'); + + expect( + StartEnterpriseAuthBodySchema.safeParse({ + ...validBody, + unifiedCreditCode: '91310000MA1K000000' + }).success + ).toBe(false); + }); + + it('银行账号需为 15-19 位数字,并在入参解析时去除空格', () => { + expect(StartEnterpriseAuthBodySchema.parse(validBody).bankAccount).toBe('4111111111111111'); + expect( + StartEnterpriseAuthBodySchema.parse({ + ...validBody, + bankAccount: '571919104910201' + }).bankAccount + ).toBe('571919104910201'); + + ['4111-1111-1111-1111', '1'.repeat(14), '1'.repeat(20)].forEach((bankAccount) => { + expect( + StartEnterpriseAuthBodySchema.safeParse({ + ...validBody, + bankAccount + }).success + ).toBe(false); + }); + }); +}); diff --git a/packages/service/common/middle/tracks/utils.ts b/packages/service/common/middle/tracks/utils.ts index a40b73f530aa..566a17041406 100644 --- a/packages/service/common/middle/tracks/utils.ts +++ b/packages/service/common/middle/tracks/utils.ts @@ -9,6 +9,11 @@ import { type ShortUrlParams } from '@fastgpt/global/support/marketing/type'; import { getRedisCache, setRedisCache } from '../../redis/cache'; import { differenceInDays } from 'date-fns'; import { getLogger, LogCategories } from '../../logger'; +import type { + TeamEnterpriseAuthStatusEnum, + TeamEnterpriseAuthTaskStatusEnum +} from '@fastgpt/global/support/user/team/enterpriseAuth/constant'; +import type { StandardSubLevelEnum } from '@fastgpt/global/support/wallet/sub/constants'; const logger = getLogger(LogCategories.EVENT.TRACK); @@ -160,6 +165,37 @@ export const pushTrack = { } }); }, + enterpriseAuthStart: ( + data: PushTrackCommonType & { + result: 'success' | 'failed'; + status?: `${TeamEnterpriseAuthStatusEnum}`; + taskStatus?: `${TeamEnterpriseAuthTaskStatusEnum}`; + errorCode?: string; + usedTimes?: number; + hasCurrentTask?: boolean; + } + ) => { + return createTrack({ + event: TrackEnum.enterpriseAuthStart, + data + }); + }, + enterpriseAuthBenefitGrant: ( + data: PushTrackCommonType & { + status?: `${TeamEnterpriseAuthStatusEnum}`; + taskId?: string; + billId?: string; + standSubLevel: `${StandardSubLevelEnum}`; + durationDay: number; + totalPoints: number; + grantedPlanCount: number; + } + ) => { + return createTrack({ + event: TrackEnum.enterpriseAuthBenefitGrant, + data + }); + }, // Admin cron job tracks subscriptionDeleted: (data: { diff --git a/packages/service/common/system/timerLock/constants.ts b/packages/service/common/system/timerLock/constants.ts index ebb2cd8b77b4..c07287e4f905 100644 --- a/packages/service/common/system/timerLock/constants.ts +++ b/packages/service/common/system/timerLock/constants.ts @@ -16,6 +16,7 @@ export enum TimerIdEnum { datasetSyncSchedulerReconcile = 'datasetSyncSchedulerReconcile', archiveInactiveSandboxes = 'archiveInactiveSandboxes', clearStaleArchivingSandboxes = 'clearStaleArchivingSandboxes', + enterpriseAuthTaskCleanup = 'enterpriseAuthTaskCleanup', /** 纠正长时间卡在 generating 的会话状态 */ cleanStaleGeneratingChat = 'cleanStaleGeneratingChat' } diff --git a/packages/web/components/common/MySelect/index.tsx b/packages/web/components/common/MySelect/index.tsx index 37bec69a9f84..f8ba06979b7a 100644 --- a/packages/web/components/common/MySelect/index.tsx +++ b/packages/web/components/common/MySelect/index.tsx @@ -100,6 +100,7 @@ const MySelect = ( const MenuListRef = useRef(null); const SelectedItemRef = useRef(null); const SearchInputRef = useRef(null); + const ignoreNextSearchSpaceClickRef = useRef(false); const { isOpen, onOpen: defaultOnOpen, onClose: defaultOnClose } = useDisclosure(); const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]); @@ -115,6 +116,47 @@ const MySelect = ( }; const [search, setSearch] = useState(''); + const isComposingSearch = (e: React.KeyboardEvent) => + e.nativeEvent.isComposing || e.keyCode === 229; + + const handleSearchSpaceKeyDown = (e: React.KeyboardEvent) => { + if (!isSearch || !isOpen || (e.key !== ' ' && e.code !== 'Space')) return; + + e.stopPropagation(); + ignoreNextSearchSpaceClickRef.current = true; + + if (isComposingSearch(e)) { + return; + } + + e.preventDefault(); + const input = e.currentTarget; + const selectionStart = input.selectionStart ?? search.length; + const selectionEnd = input.selectionEnd ?? selectionStart; + const nextSearch = `${search.slice(0, selectionStart)} ${search.slice(selectionEnd)}`; + + setSearch(nextSearch); + window.requestAnimationFrame(() => { + const nextPosition = selectionStart + 1; + input.setSelectionRange(nextPosition, nextPosition); + ignoreNextSearchSpaceClickRef.current = false; + }); + }; + + const handleSearchSpaceKeyUp = (e: React.KeyboardEvent) => { + if (!isSearch || !isOpen || (e.key !== ' ' && e.code !== 'Space')) return; + + e.stopPropagation(); + ignoreNextSearchSpaceClickRef.current = true; + + if (!isComposingSearch(e)) { + e.preventDefault(); + } + + window.setTimeout(() => { + ignoreNextSearchSpaceClickRef.current = false; + }, 0); + }; const filterList = useMemo(() => { if (!isSearch || !search) { return list; @@ -140,6 +182,7 @@ const MySelect = ( menu.scrollTop = selectedItem.offsetTop - menu.offsetTop - 100; if (isSearch) { + // eslint-disable-next-line react-hooks/set-state-in-effect setSearch(''); } } @@ -242,6 +285,14 @@ const MySelect = ( opacity={isDisabled ? 0.4 : 1} _hover={isInvalid ? { borderColor: 'red.400' } : { borderColor: 'primary.300' }} {...props} + onClickCapture={(e) => { + props.onClickCapture?.(e); + if (e.isPropagationStopped() || !ignoreNextSearchSpaceClickRef.current) return; + + ignoreNextSearchSpaceClickRef.current = false; + e.preventDefault(); + e.stopPropagation(); + }} > @@ -267,6 +318,8 @@ const MySelect = ( size={'sm'} w={'100%'} color={'myGray.700'} + onKeyDown={handleSearchSpaceKeyDown} + onKeyUp={handleSearchSpaceKeyUp} onBlur={() => { setTimeout(() => { SearchInputRef?.current?.focus(); @@ -306,10 +359,12 @@ const MySelect = ( ref={MenuListRef} className={props.className} minW={(() => { + /* eslint-disable react-hooks/refs */ const w = ButtonRef.current?.clientWidth; if (w) { return `${w}px !important`; } + /* eslint-enable react-hooks/refs */ return Array.isArray(width) ? width.map((item) => `${item} !important`) : `${width} !important`; diff --git a/packages/web/i18n/en/account_team.json b/packages/web/i18n/en/account_team.json index 0db672936ae0..13b3d9477575 100644 --- a/packages/web/i18n/en/account_team.json +++ b/packages/web/i18n/en/account_team.json @@ -43,6 +43,9 @@ "copy_api_key": "Copy API key", "copy_link": "Copy link", "create_api_key": "Create API key", + "start_enterprise_auth": "Start enterprise verification", + "verify_enterprise_auth_amount": "Verify enterprise amount", + "reset_enterprise_auth_task": "Refill enterprise verification info", "create_app": "Create an application", "create_app_copy": "Create a copy of the application", "create_app_folder": "Create an application folder", @@ -149,6 +152,9 @@ "log_change_password": "【{{name}}】The password change operation was performed", "log_copy_api_key": "【{{name}}】Copied the API key named [{{keyName}}]", "log_create_api_key": "【{{name}}】Create an API key named [{{keyName}}]", + "log_start_enterprise_auth": "【{{name}}】Started verification for enterprise 【{{enterpriseName}}】", + "log_verify_enterprise_auth_amount": "【{{name}}】Verified the transfer amount for enterprise 【{{enterpriseName}}】", + "log_reset_enterprise_auth_task": "【{{name}}】Refilled verification info for enterprise 【{{enterpriseName}}】", "log_create_app": "【{{name}}】Created [{{appType}}] named [{{appName}}]", "log_create_app_copy": "【{{name}}] Created a copy of [{{appType}}] named [{{appName}}]", "log_create_app_folder": "【{{name}}】Create a folder named [{{folderName}}]", @@ -295,5 +301,55 @@ "log_transfer_skill_ownership": "[{{name}}] Transferred ownership of [{{skillType}}] named [{{skillName}}] from [{{oldOwnerName}}] to [{{newOwnerName}}]", "log_update_skill": "[{{name}}] Updated [{{skillType}}] named [{{skillName}}]", "log_update_skill_collaborator": "[{{name}}] Updated collaborators for [{{skillType}}] named [{{skillName}}] to: Organization: [{{orgList}}], Group: [{{groupList}}], Member [{{tmbList}}]; permissions updated to: [{{permission}}]", + "enterprise_auth_title": "Enterprise verification", + "enterprise_auth_verified_label": "Verification completed", + "enterprise_auth_pending_amount_label": "Waiting for transfer amount", + "enterprise_auth_processing_label": "Verification is processing", + "enterprise_auth_continue_button": "Continue", + "enterprise_auth_unverified_label": "Not verified (verify to get Advanced trial)", + "enterprise_auth_button": "Verify", + "enterprise_auth_contact_admin_tip": "Contact a team admin to continue", + "enterprise_auth_bank_load_failed": "Failed to load bank list. Try again later.", + "enterprise_auth_bank_retry": "Retry", + "enterprise_auth_task_load_failed": "Failed to load verification task", + "enterprise_auth_submit_failed": "Failed to submit verification", + "enterprise_auth_no_remaining_times_tip": "Your team's verification attempts have been used up. Contact sales to complete enterprise verification.", + "enterprise_auth_verify_failed": "Verification failed", + "enterprise_auth_operation_failed": "Operation failed", + "enterprise_auth_success_grant_tip": "Enterprise verified. Advanced benefits have been granted.", + "enterprise_auth_transfer_sent_tip": "Transfer sent. Please confirm the received amount.", + "enterprise_auth_invalid_amount_tip": "Enter a valid received amount", + "enterprise_auth_modal_desc": "Complete enterprise verification with a small transfer. After verification succeeds, you will receive a 15-day Advanced plan trial.", + "enterprise_auth_enterprise_info": "Enterprise information", + "enterprise_auth_enterprise_name": "Enterprise name", + "enterprise_auth_enterprise_name_placeholder": "Enter enterprise name", + "enterprise_auth_unified_credit_code": "Unified social credit code", + "enterprise_auth_unified_credit_code_placeholder": "Enter unified credit code", + "enterprise_auth_legal_person": "Legal representative", + "enterprise_auth_legal_person_placeholder": "Enter the legal representative", + "enterprise_auth_bank_account": "Bank account", + "enterprise_auth_bank_account_placeholder": "Enter bank account", + "enterprise_auth_bank_name": "Bank", + "enterprise_auth_bank_name_placeholder": "Select the bank head office", + "enterprise_auth_bank_loading_placeholder": "Loading bank list", + "enterprise_auth_invalid_format_tip": "Enter valid information", + "enterprise_auth_contact_info": "Personal information", + "enterprise_auth_contact_name": "Your name", + "enterprise_auth_contact_name_placeholder": "Enter how we should address you", + "enterprise_auth_contact_title": "Your title", + "enterprise_auth_contact_title_placeholder": "Enter your title", + "enterprise_auth_contact_phone": "Contact phone", + "enterprise_auth_contact_phone_placeholder": "Enter your phone number, only for business contact", + "enterprise_auth_demand": "Your needs", + "enterprise_auth_demand_placeholder": "Describe how you and your company use FastGPT so we can provide better service", + "enterprise_auth_amount_sent_prefix": "A transfer has been initiated to the account above. The verification amount will be sent by Shanghai UnionPay, is random, usually arrives in real time, and is valid for 24 hours. If it has not arrived, please ", + "enterprise_auth_contact_business": "contact sales", + "enterprise_auth_amount_sent_suffix": ".", + "enterprise_auth_amount_label": "Verification amount ¥", + "enterprise_auth_amount_error_tip": "Incorrect amount", + "enterprise_auth_reset_info": "Refill enterprise info", + "enterprise_auth_cancel": "Cancel", + "enterprise_auth_start": "Start verification", + "enterprise_auth_verify_with_remaining": "Verify ({{count}} attempts left)", "move_skill": "Move skill" } diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 5de5b5bf2e06..fe560278177c 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -189,6 +189,35 @@ "code_error.system_error.license_app_amount_limit": "Exceed the maximum number of applications in the system", "code_error.system_error.license_dataset_amount_limit": "Exceed the maximum number of knowledge bases in the system", "code_error.system_error.license_user_amount_limit": "Exceed the maximum number of users in the system", + "enterprise_auth.error.disabled": "Enterprise verification is disabled", + "enterprise_auth.error.service_not_configured": "Enterprise verification service is not configured", + "enterprise_auth.error.no_remaining_times": "No verification attempts remaining", + "enterprise_auth.error.already_verified": "This team or enterprise has already been verified", + "enterprise_auth.error.enterprise_occupied": "This enterprise is being verified or has already been verified", + "enterprise_auth.error.too_frequent": "Too many attempts. Please try again later.", + "enterprise_auth.error.service_error": "Verification service error. Please try again later.", + "enterprise_auth.error.service_timeout": "Service request timed out. Please try again later.", + "enterprise_auth.error.info_failed": "Verification information is incorrect. Please fill it in again.", + "enterprise_auth.error.task_not_found": "Verification task does not exist or has ended", + "enterprise_auth.error.task_expired": "Verification task has expired. Please fill it in again.", + "enterprise_auth.error.amount_error": "Incorrect verification amount", + "enterprise_auth.error.amount_failed": "Too many incorrect amount attempts. This verification failed.", + "enterprise_auth.error.processing": "Verification is processing. Please try again later.", + "enterprise_auth_notice_title": "Enterprise verification benefits", + "enterprise_auth_notice_headline": "Verify now to claim an Advanced plan", + "enterprise_auth_notice_greeting": "Dear user,", + "enterprise_auth_notice_intro": "To support enterprise growth, FastGPT is now offering enterprise verification benefits.", + "enterprise_auth_notice_benefit_intro": "After successful verification, your account will be automatically upgraded with Advanced benefits worth RMB 450:", + "enterprise_auth_notice_benefit_advanced": "Unlock a 15-day Advanced plan with more premium capabilities", + "enterprise_auth_notice_benefit_points": "AI points included with the Advanced plan", + "enterprise_auth_notice_benefit_support": "Dedicated enterprise-level service support", + "enterprise_auth_notice_entry": "Verification entry: Account - Personal info - Enterprise verification", + "enterprise_auth_notice_or_click": ", or click ", + "enterprise_auth_notice_link": "this link", + "enterprise_auth_notice_link_suffix": " to go there directly", + "enterprise_auth_notice_help": "If you have any questions, contact support at any time. Thank you for your support.", + "enterprise_auth_notice_footer": "Professional tools for enterprise growth", + "enterprise_auth_notice_read": "Read", "code_error.team_error.ai_points_not_enough": "Insufficient AI Points", "code_error.team_error.app_amount_not_enough": "Application Limit Reached", "code_error.team_error.app_folder_amount_not_enough": "Folder Limit Reached", @@ -1030,12 +1059,14 @@ "support.wallet.bill.payWay.alipay": "Alipay Payment", "support.wallet.bill.payWay.balance": "Balance Payment", "support.wallet.bill.payWay.bank": "Bank Transfer", + "support.wallet.bill.payWay.enterpriseAuth": "Enterprise verification grant", "support.wallet.bill.payWay.wecom": "WeChat Work Payment", "support.wallet.bill.payWay.wx": "WeChat Payment", "support.wallet.bill.status.closed": "Closed", "support.wallet.bill.status.notpay": "Unpaid", "support.wallet.bill.status.refund": "Refunded", "support.wallet.bill.status.success": "Payment Successful", + "support.wallet.bill.type.activityGift": "Activity Gift", "support.wallet.buy_dataset_capacity": "Buy Knowledge Base Index", "support.wallet.subscription.AI points usage": "AI Points Usage", "support.wallet.subscription.Activity expiration time": "Activity ends on {{month}}/{{day}}/{{year}} at {{hour}}:{{minute}}", diff --git a/packages/web/i18n/zh-CN/account_team.json b/packages/web/i18n/zh-CN/account_team.json index fd8c47b5b8a3..7040ad153bd8 100644 --- a/packages/web/i18n/zh-CN/account_team.json +++ b/packages/web/i18n/zh-CN/account_team.json @@ -43,6 +43,9 @@ "copy_api_key": "复制api密钥", "copy_link": "复制链接", "create_api_key": "创建api密钥", + "start_enterprise_auth": "发起企业认证", + "verify_enterprise_auth_amount": "验证企业认证金额", + "reset_enterprise_auth_task": "重新填写企业认证信息", "create_app": "创建应用", "create_app_copy": "创建应用副本", "create_app_folder": "创建应用文件夹", @@ -147,6 +150,9 @@ "log_change_password": "【{{name}}】进行了变更密码操作", "log_copy_api_key": "【{{name}}】复制了名为【{{keyName}}】的api密钥", "log_create_api_key": "【{{name}}】创建了名为【{{keyName}}】的api密钥", + "log_start_enterprise_auth": "【{{name}}】发起了企业【{{enterpriseName}}】认证", + "log_verify_enterprise_auth_amount": "【{{name}}】通过了企业【{{enterpriseName}}】认证金额验证", + "log_reset_enterprise_auth_task": "【{{name}}】重新填写企业【{{enterpriseName}}】认证信息", "log_create_app": "【{{name}}】创建了名为【{{appName}}】的【{{appType}}】", "log_create_app_copy": "【{{name}}】给名为【{{appName}}】的【{{appType}}】创建了一个副本", "log_create_app_folder": "【{{name}}】创建了名为【{{folderName}}】的文件夹", @@ -295,5 +301,55 @@ "log_transfer_skill_ownership": "【{{name}}】将名为【{{skillName}}】的【{{skillType}}】的所有权从【{{oldOwnerName}}】转移到【{{newOwnerName}}】", "log_update_skill": "【{{name}}】更新了名为【{{skillName}}】的【{{skillType}}】", "log_update_skill_collaborator": "【{{name}}】将名为【{{skillName}}】的【{{skillType}}】的合作者更新为:组织:【{{orgList}}】,群组:【{{groupList}}】,成员【{{tmbList}}】;权限更新为:【{{permission}}】", + "enterprise_auth_title": "企业认证", + "enterprise_auth_verified_label": "已完成认证", + "enterprise_auth_pending_amount_label": "待确认打款金额", + "enterprise_auth_processing_label": "认证处理中", + "enterprise_auth_continue_button": "继续认证", + "enterprise_auth_unverified_label": "未认证(认证享高级套餐)", + "enterprise_auth_button": "认证", + "enterprise_auth_contact_admin_tip": "请联系团队管理员操作", + "enterprise_auth_bank_load_failed": "获取银行列表失败,请稍后重试", + "enterprise_auth_bank_retry": "重试", + "enterprise_auth_task_load_failed": "获取认证任务失败", + "enterprise_auth_submit_failed": "认证提交失败", + "enterprise_auth_no_remaining_times_tip": "团队的认证次数已用尽,请联系商务人员进行企业认证", + "enterprise_auth_verify_failed": "验证失败", + "enterprise_auth_operation_failed": "操作失败", + "enterprise_auth_success_grant_tip": "企业认证通过,已发放高级版权益", + "enterprise_auth_transfer_sent_tip": "已成功打款,请确认打款金额", + "enterprise_auth_invalid_amount_tip": "请输入正确的到账金额", + "enterprise_auth_modal_desc": "通过小额打款进行企业认证,认证成功后将为您发放15天的高级版套餐", + "enterprise_auth_enterprise_info": "企业信息", + "enterprise_auth_enterprise_name": "企业名称", + "enterprise_auth_enterprise_name_placeholder": "请填写企业名称", + "enterprise_auth_unified_credit_code": "统一社会信用代码", + "enterprise_auth_unified_credit_code_placeholder": "请填写统一信用代码", + "enterprise_auth_legal_person": "法人姓名", + "enterprise_auth_legal_person_placeholder": "请输入法人姓名", + "enterprise_auth_bank_account": "银行账号", + "enterprise_auth_bank_account_placeholder": "请填写银行账号", + "enterprise_auth_bank_name": "开户银行", + "enterprise_auth_bank_name_placeholder": "请选择开户银行总行", + "enterprise_auth_bank_loading_placeholder": "银行列表加载中", + "enterprise_auth_invalid_format_tip": "请填写正确的信息", + "enterprise_auth_contact_info": "个人信息", + "enterprise_auth_contact_name": "您的姓名", + "enterprise_auth_contact_name_placeholder": "请填写您的称呼方式", + "enterprise_auth_contact_title": "您的职位", + "enterprise_auth_contact_title_placeholder": "请填写您的职位身份", + "enterprise_auth_contact_phone": "联系方式", + "enterprise_auth_contact_phone_placeholder": "请填写您的手机号码,仅用于商务人员联系", + "enterprise_auth_demand": "您的需求", + "enterprise_auth_demand_placeholder": "请描述您及贵司使用FastGPT的场景,以便我们更好地为您提供服务", + "enterprise_auth_amount_sent_prefix": "已向以上账号成功发起打款,验证金额将由上海银联打入,金额随机,一般实时到账,有效期24小时。若未到账,请", + "enterprise_auth_contact_business": "联系商务", + "enterprise_auth_amount_sent_suffix": "。", + "enterprise_auth_amount_label": "验证金额¥", + "enterprise_auth_amount_error_tip": "金额错误", + "enterprise_auth_reset_info": "重新填写企业信息", + "enterprise_auth_cancel": "取消", + "enterprise_auth_start": "开始认证", + "enterprise_auth_verify_with_remaining": "验证(剩余{{count}}次)", "move_skill": "移动技能" } diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 4ae62bb821a1..90591ea0b7fd 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -189,6 +189,35 @@ "code_error.system_error.license_app_amount_limit": "超出系统最大应用数量", "code_error.system_error.license_dataset_amount_limit": "超出系统最大知识库数量", "code_error.system_error.license_user_amount_limit": "超出系统最大用户数量", + "enterprise_auth.error.disabled": "企业认证功能未开启", + "enterprise_auth.error.service_not_configured": "企业认证服务未配置", + "enterprise_auth.error.no_remaining_times": "认证次数已用完", + "enterprise_auth.error.already_verified": "该团队或企业已完成认证", + "enterprise_auth.error.enterprise_occupied": "该企业正在认证或已被认证", + "enterprise_auth.error.too_frequent": "操作过于频繁,请稍后再试", + "enterprise_auth.error.service_error": "验证服务错误,请稍后重试", + "enterprise_auth.error.service_timeout": "服务网络超时,请稍后重试", + "enterprise_auth.error.info_failed": "认证信息错误,请重新填写", + "enterprise_auth.error.task_not_found": "认证任务不存在或已结束", + "enterprise_auth.error.task_expired": "认证任务已过期,请重新填写", + "enterprise_auth.error.amount_error": "验证金额错误", + "enterprise_auth.error.amount_failed": "验证金额错误次数已达上限,本次认证失败", + "enterprise_auth.error.processing": "认证处理中,请稍后重试", + "enterprise_auth_notice_title": "企业认证福利", + "enterprise_auth_notice_headline": "立即认证,领取高级套餐", + "enterprise_auth_notice_greeting": "尊敬的用户:", + "enterprise_auth_notice_intro": "为助力企业发展,FastGPT现推出企业认证福利", + "enterprise_auth_notice_benefit_intro": "认证成功后,您的账号将自动升级,享受价值450元的高级权益:", + "enterprise_auth_notice_benefit_advanced": "解锁15天高级版套餐,享受更多高级功能权限", + "enterprise_auth_notice_benefit_points": "高级套餐配套 AI 积分", + "enterprise_auth_notice_benefit_support": "专属企业级服务支持", + "enterprise_auth_notice_entry": "【认证入口:账号-个人信息-企业认证】", + "enterprise_auth_notice_or_click": ",或点击", + "enterprise_auth_notice_link": "链接", + "enterprise_auth_notice_link_suffix": ",一键跳转认证", + "enterprise_auth_notice_help": "如有疑问,可随时联系客服协助处理。感谢您的支持!", + "enterprise_auth_notice_footer": "让专业工具,助力企业成长", + "enterprise_auth_notice_read": "已读", "code_error.team_error.ai_points_not_enough": "AI 积分不足", "code_error.team_error.app_amount_not_enough": "应用数量已达上限~", "code_error.team_error.app_folder_amount_not_enough": "文件夹数量已达上限~", @@ -1030,12 +1059,14 @@ "support.wallet.bill.payWay.alipay": "支付宝支付", "support.wallet.bill.payWay.balance": "余额支付", "support.wallet.bill.payWay.bank": "对公支付", + "support.wallet.bill.payWay.enterpriseAuth": "企业认证赠送", "support.wallet.bill.payWay.wecom": "企业微信支付", "support.wallet.bill.payWay.wx": "微信支付", "support.wallet.bill.status.closed": "已关闭", "support.wallet.bill.status.notpay": "未支付", "support.wallet.bill.status.refund": "已退款", "support.wallet.bill.status.success": "支付成功", + "support.wallet.bill.type.activityGift": "活动赠送", "support.wallet.buy_dataset_capacity": "购买知识库索引量", "support.wallet.subscription.AI points usage": "AI 积分使用量", "support.wallet.subscription.Activity expiration time": "活动截至{{year}}年{{month}}月{{day}}日{{hour}}:{{minute}}", diff --git a/packages/web/i18n/zh-Hant/account_team.json b/packages/web/i18n/zh-Hant/account_team.json index 0cb856eaffe9..cd9317d5e21c 100644 --- a/packages/web/i18n/zh-Hant/account_team.json +++ b/packages/web/i18n/zh-Hant/account_team.json @@ -43,6 +43,9 @@ "copy_api_key": "複製api密鑰", "copy_link": "複製連結", "create_api_key": "創建api密鑰", + "start_enterprise_auth": "發起企業認證", + "verify_enterprise_auth_amount": "驗證企業認證金額", + "reset_enterprise_auth_task": "重新填寫企業認證資訊", "create_app": "創建應用", "create_app_copy": "創建應用副本", "create_app_folder": "創建應用文件夾", @@ -147,6 +150,9 @@ "log_change_password": "【{{name}}】進行了變更密碼操作", "log_copy_api_key": "【{{name}}】複製了名為【{{keyName}}】的api密鑰", "log_create_api_key": "【{{name}}】創建了名為【{{keyName}}】的api密鑰", + "log_start_enterprise_auth": "【{{name}}】發起了企業【{{enterpriseName}}】認證", + "log_verify_enterprise_auth_amount": "【{{name}}】通過了企業【{{enterpriseName}}】認證金額驗證", + "log_reset_enterprise_auth_task": "【{{name}}】重新填寫企業【{{enterpriseName}}】認證資訊", "log_create_app": "【{{name}}】創建了名為【{{appName}}】的【{{appType}}】", "log_create_app_copy": "【{{name}}】給名為【{{appName}}】的【{{appType}}】創建了一個副本", "log_create_app_folder": "【{{name}}】創建了名為【{{folderName}}】的文件夾", @@ -291,5 +297,55 @@ "log_transfer_skill_ownership": "【{{name}}】將名為【{{skillName}}】的【{{skillType}}】的所有權從【{{oldOwnerName}}】轉移到【{{newOwnerName}}】", "log_update_skill": "【{{name}}】更新了名為【{{skillName}}】的【{{skillType}}】", "log_update_skill_collaborator": "【{{name}}】將名為【{{skillName}}】的【{{skillType}}】的合作者更新為:組織:【{{orgList}}】,群組:【{{groupList}}】,成員【{{tmbList}}】;權限更新為:【{{permission}}】", + "enterprise_auth_title": "企業認證", + "enterprise_auth_verified_label": "已完成認證", + "enterprise_auth_pending_amount_label": "待確認打款金額", + "enterprise_auth_processing_label": "認證處理中", + "enterprise_auth_continue_button": "繼續認證", + "enterprise_auth_unverified_label": "未認證(認證享高級套餐)", + "enterprise_auth_button": "認證", + "enterprise_auth_contact_admin_tip": "請聯絡團隊管理員操作", + "enterprise_auth_bank_load_failed": "獲取銀行列表失敗,請稍後重試", + "enterprise_auth_bank_retry": "重試", + "enterprise_auth_task_load_failed": "獲取認證任務失敗", + "enterprise_auth_submit_failed": "認證提交失敗", + "enterprise_auth_no_remaining_times_tip": "團隊的認證次數已用盡,請聯絡商務人員進行企業認證", + "enterprise_auth_verify_failed": "驗證失敗", + "enterprise_auth_operation_failed": "操作失敗", + "enterprise_auth_success_grant_tip": "企業認證通過,已發放高級版權益", + "enterprise_auth_transfer_sent_tip": "已成功打款,請確認打款金額", + "enterprise_auth_invalid_amount_tip": "請輸入正確的到賬金額", + "enterprise_auth_modal_desc": "透過小額打款進行企業認證,認證成功後將為您發放15天的高級版套餐", + "enterprise_auth_enterprise_info": "企業資訊", + "enterprise_auth_enterprise_name": "企業名稱", + "enterprise_auth_enterprise_name_placeholder": "請填寫企業名稱", + "enterprise_auth_unified_credit_code": "統一社會信用代碼", + "enterprise_auth_unified_credit_code_placeholder": "請填寫統一信用代碼", + "enterprise_auth_legal_person": "法人姓名", + "enterprise_auth_legal_person_placeholder": "請輸入法人姓名", + "enterprise_auth_bank_account": "銀行帳號", + "enterprise_auth_bank_account_placeholder": "請填寫銀行帳號", + "enterprise_auth_bank_name": "開戶銀行", + "enterprise_auth_bank_name_placeholder": "請選擇開戶銀行總行", + "enterprise_auth_bank_loading_placeholder": "銀行列表載入中", + "enterprise_auth_invalid_format_tip": "請填寫正確的資訊", + "enterprise_auth_contact_info": "個人資訊", + "enterprise_auth_contact_name": "您的姓名", + "enterprise_auth_contact_name_placeholder": "請填寫您的稱呼方式", + "enterprise_auth_contact_title": "您的職位", + "enterprise_auth_contact_title_placeholder": "請填寫您的職位身分", + "enterprise_auth_contact_phone": "聯絡方式", + "enterprise_auth_contact_phone_placeholder": "請填寫您的手機號碼,僅用於商務人員聯繫", + "enterprise_auth_demand": "您的需求", + "enterprise_auth_demand_placeholder": "請描述您及貴司使用FastGPT的場景,以便我們更好地為您提供服務", + "enterprise_auth_amount_sent_prefix": "已向以上帳號成功發起打款,驗證金額將由上海銀聯打入,金額隨機,一般即時到賬,有效期24小時。若未到賬,請", + "enterprise_auth_contact_business": "聯絡商務", + "enterprise_auth_amount_sent_suffix": "。", + "enterprise_auth_amount_label": "驗證金額¥", + "enterprise_auth_amount_error_tip": "金額錯誤", + "enterprise_auth_reset_info": "重新填寫企業資訊", + "enterprise_auth_cancel": "取消", + "enterprise_auth_start": "開始認證", + "enterprise_auth_verify_with_remaining": "驗證(剩餘{{count}}次)", "move_skill": "移動技能" } diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 16c0a9abfc1a..d649bea3d5d9 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -187,6 +187,35 @@ "code_error.system_error.license_app_amount_limit": "超出系統最大應用數量", "code_error.system_error.license_dataset_amount_limit": "超出系統最大知識庫數量", "code_error.system_error.license_user_amount_limit": "超出系統最大用戶數量", + "enterprise_auth.error.disabled": "企業認證功能未開啟", + "enterprise_auth.error.service_not_configured": "企業認證服務未配置", + "enterprise_auth.error.no_remaining_times": "認證次數已用完", + "enterprise_auth.error.already_verified": "該團隊或企業已完成認證", + "enterprise_auth.error.enterprise_occupied": "該企業正在認證或已被認證", + "enterprise_auth.error.too_frequent": "操作過於頻繁,請稍後再試", + "enterprise_auth.error.service_error": "驗證服務錯誤,請稍後重試", + "enterprise_auth.error.service_timeout": "服務網路逾時,請稍後重試", + "enterprise_auth.error.info_failed": "認證資訊錯誤,請重新填寫", + "enterprise_auth.error.task_not_found": "認證任務不存在或已結束", + "enterprise_auth.error.task_expired": "認證任務已過期,請重新填寫", + "enterprise_auth.error.amount_error": "驗證金額錯誤", + "enterprise_auth.error.amount_failed": "驗證金額錯誤次數已達上限,本次認證失敗", + "enterprise_auth.error.processing": "認證處理中,請稍後重試", + "enterprise_auth_notice_title": "企業認證福利", + "enterprise_auth_notice_headline": "立即認證,領取高級套餐", + "enterprise_auth_notice_greeting": "尊敬的使用者:", + "enterprise_auth_notice_intro": "為助力企業發展,FastGPT 現推出企業認證福利", + "enterprise_auth_notice_benefit_intro": "認證成功後,您的帳號將自動升級,享受價值450元的高級權益:", + "enterprise_auth_notice_benefit_advanced": "解鎖15天高級版套餐,享受更多高級功能權限", + "enterprise_auth_notice_benefit_points": "高級套餐配套 AI 點數", + "enterprise_auth_notice_benefit_support": "專屬企業級服務支援", + "enterprise_auth_notice_entry": "【認證入口:帳號-個人資訊-企業認證】", + "enterprise_auth_notice_or_click": ",或點擊", + "enterprise_auth_notice_link": "連結", + "enterprise_auth_notice_link_suffix": ",一鍵跳轉認證", + "enterprise_auth_notice_help": "如有疑問,可隨時聯絡客服協助處理。感謝您的支持!", + "enterprise_auth_notice_footer": "讓專業工具,助力企業成長", + "enterprise_auth_notice_read": "已讀", "code_error.team_error.ai_points_not_enough": "AI 點數不足", "code_error.team_error.app_amount_not_enough": "已達應用程式數量上限", "code_error.team_error.app_folder_amount_not_enough": "已達資料夾數量上限", @@ -1019,12 +1048,14 @@ "support.wallet.bill.payWay.alipay": "支付寶支付", "support.wallet.bill.payWay.balance": "餘額支付", "support.wallet.bill.payWay.bank": "對公支付", + "support.wallet.bill.payWay.enterpriseAuth": "企業認證贈送", "support.wallet.bill.payWay.wecom": "企業微信支付", "support.wallet.bill.payWay.wx": "微信支付", "support.wallet.bill.status.closed": "已關閉", "support.wallet.bill.status.notpay": "未付款", "support.wallet.bill.status.refund": "已退款", "support.wallet.bill.status.success": "付款成功", + "support.wallet.bill.type.activityGift": "活動贈送", "support.wallet.buy_dataset_capacity": "購買知識庫索引量", "support.wallet.subscription.AI points usage": "AI 點數使用量", "support.wallet.subscription.Activity expiration time": "活動截至{{year}}年{{month}}月{{day}}日{{hour}}:{{minute}}", diff --git a/packages/web/support/user/audit/constants.ts b/packages/web/support/user/audit/constants.ts index e22df17bc562..ce6a2aaafaff 100644 --- a/packages/web/support/user/audit/constants.ts +++ b/packages/web/support/user/audit/constants.ts @@ -496,6 +496,21 @@ export const auditLogMap = { typeLabel: i18nT('account_team:set_invoice_header'), params: {} as { name?: string } }, + [AuditEventEnum.START_ENTERPRISE_AUTH]: { + content: i18nT('account_team:log_start_enterprise_auth'), + typeLabel: i18nT('account_team:start_enterprise_auth'), + params: {} as { name?: string; enterpriseName: string } + }, + [AuditEventEnum.VERIFY_ENTERPRISE_AUTH_AMOUNT]: { + content: i18nT('account_team:log_verify_enterprise_auth_amount'), + typeLabel: i18nT('account_team:verify_enterprise_auth_amount'), + params: {} as { name?: string; enterpriseName: string } + }, + [AuditEventEnum.RESET_ENTERPRISE_AUTH_TASK]: { + content: i18nT('account_team:log_reset_enterprise_auth_task'), + typeLabel: i18nT('account_team:reset_enterprise_auth_task'), + params: {} as { name?: string; enterpriseName: string } + }, [AuditEventEnum.CREATE_API_KEY]: { content: i18nT('account_team:log_create_api_key'), typeLabel: i18nT('account_team:create_api_key'), diff --git a/pro b/pro index 2b3e11934332..32ff3fa6772f 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 2b3e11934332dcefae9ee6847f65a2e15d32095a +Subproject commit 32ff3fa6772fa67463ad64a080a568e620433e8f diff --git a/projects/app/src/components/Layout/index.tsx b/projects/app/src/components/Layout/index.tsx index 425a06d31b89..2e4f79047f76 100644 --- a/projects/app/src/components/Layout/index.tsx +++ b/projects/app/src/components/Layout/index.tsx @@ -30,6 +30,12 @@ const NotSufficientModal = dynamic(() => import('@/components/support/wallet/Not const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'), { ssr: false }); +const EnterpriseAuthNoticeModal = dynamic( + () => import('@/components/support/user/inform/EnterpriseAuthNoticeModal'), + { + ssr: false + } +); const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'), { ssr: false }); @@ -133,13 +139,17 @@ const Layout = ({ children }: { children: JSX.Element }) => { status: 'warning', title: t('common:llm_model_not_config') }); - router.pathname !== '/account/model' && router.push('/account/model'); + if (router.pathname !== '/account/model') { + router.push('/account/model'); + } } else if (embeddingModelList.length === 0) { toast({ status: 'warning', title: t('common:embedding_model_not_config') }); - router.pathname !== '/account/model' && router.push('/account/model'); + if (router.pathname !== '/account/model') { + router.push('/account/model'); + } } } }, @@ -152,7 +162,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { // Route watch useEffect(() => { setLastRoute(router.pathname); - }, [router.pathname]); + }, [router.pathname, setLastRoute]); return ( <> @@ -206,6 +216,7 @@ const Layout = ({ children }: { children: JSX.Element }) => { )} + diff --git a/projects/app/src/components/support/user/inform/EnterpriseAuthNoticeModal.tsx b/projects/app/src/components/support/user/inform/EnterpriseAuthNoticeModal.tsx new file mode 100644 index 000000000000..7611d55f3c19 --- /dev/null +++ b/projects/app/src/components/support/user/inform/EnterpriseAuthNoticeModal.tsx @@ -0,0 +1,178 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Box, Button, Flex, Link, Text } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import MyModal from '@fastgpt/web/components/v2/common/MyModal'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { useTranslation } from 'next-i18next'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { useQuery } from '@tanstack/react-query'; +import { getEnterpriseAuthStatus } from '@/web/support/user/team/enterpriseAuth/api'; +import { TeamEnterpriseAuthStatusEnum } from '@fastgpt/global/support/user/team/enterpriseAuth/constant'; +import { canManageEnterpriseAuth } from '@/pageComponents/account/team/EnterpriseAuth/utils'; + +const certificationHref = '/account/info#certification'; + +const NoteIcon = () => ( + + + +); + +const BenefitItem = ({ children }: { children: React.ReactNode }) => ( + + + + + {children} + +); + +const EnterpriseAuthNoticeModal = () => { + const router = useRouter(); + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + const { userInfo, enterpriseAuthNoticeReadTeamIds, setEnterpriseAuthNoticeRead } = useUserStore(); + const [isClosed, setIsClosed] = useState(false); + + const teamId = userInfo?.team?.teamId; + const canCheckEnterpriseAuthNotice = canManageEnterpriseAuth({ + isTeamOwner: userInfo?.team?.permission?.isOwner, + hasTeamManagePer: userInfo?.team?.permission?.hasManagePer + }); + const shouldCheckEnterpriseAuthNotice = + router.pathname === '/dashboard/agent' && + !!feConfigs?.show_enterprise_auth && + !!teamId && + canCheckEnterpriseAuthNotice && + !enterpriseAuthNoticeReadTeamIds?.includes(teamId); + const { data: enterpriseAuthStatus } = useQuery( + ['getEnterpriseAuthNoticeStatus', teamId], + getEnterpriseAuthStatus, + { + enabled: shouldCheckEnterpriseAuthNotice, + staleTime: 30000 + } + ); + const canShowEnterpriseAuthNotice = canManageEnterpriseAuth({ + statusCanManage: enterpriseAuthStatus?.canManage, + isTeamOwner: userInfo?.team?.permission?.isOwner, + hasTeamManagePer: userInfo?.team?.permission?.hasManagePer + }); + const showEnterpriseAuthNotice = useMemo( + () => + shouldCheckEnterpriseAuthNotice && + canShowEnterpriseAuthNotice && + enterpriseAuthStatus?.enabled !== false && + !!enterpriseAuthStatus?.status && + enterpriseAuthStatus.status !== TeamEnterpriseAuthStatusEnum.verified, + [ + canShowEnterpriseAuthNotice, + enterpriseAuthStatus?.enabled, + enterpriseAuthStatus?.status, + shouldCheckEnterpriseAuthNotice + ] + ); + + const markAsRead = useCallback(() => { + if (teamId) { + setEnterpriseAuthNoticeRead(teamId); + } + }, [setEnterpriseAuthNoticeRead, teamId]); + + const onClickRead = useCallback(() => { + markAsRead(); + setIsClosed(true); + }, [markAsRead]); + + const onClickCertificationLink = useCallback( + async (event: React.MouseEvent) => { + event.preventDefault(); + markAsRead(); + await router.push(certificationHref); + }, + [markAsRead, router] + ); + + if (!showEnterpriseAuthNotice || isClosed) return null; + + return ( + setIsClosed(true)} + isCentered + size={'md'} + title={t('common:enterprise_auth_notice_title')} + footer={} + > + + + + {t('common:enterprise_auth_notice_headline')} + + + {t('common:enterprise_auth_notice_greeting')} + {t('common:enterprise_auth_notice_intro')} + + + + + {t('common:enterprise_auth_notice_benefit_intro')} + + + + {t('common:enterprise_auth_notice_benefit_advanced')} + {t('common:enterprise_auth_notice_benefit_points')} + {t('common:enterprise_auth_notice_benefit_support')} + + + + + + {t('common:enterprise_auth_notice_entry')} + + {t('common:enterprise_auth_notice_or_click')} + + {t('common:enterprise_auth_notice_link')} + + {t('common:enterprise_auth_notice_link_suffix')} + + + {t('common:enterprise_auth_notice_help')} + + + + {t('common:enterprise_auth_notice_footer')} + + + + ); +}; + +export default React.memo(EnterpriseAuthNoticeModal); diff --git a/projects/app/src/components/support/user/inform/SystemMsgModal.tsx b/projects/app/src/components/support/user/inform/SystemMsgModal.tsx index dc21d97477c5..9216da075923 100644 --- a/projects/app/src/components/support/user/inform/SystemMsgModal.tsx +++ b/projects/app/src/components/support/user/inform/SystemMsgModal.tsx @@ -10,7 +10,7 @@ import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { webPushTrack } from '@/web/common/middle/tracks/utils'; const Markdown = dynamic(() => import('@/components/Markdown'), { ssr: false }); -const SystemMsgModal = ({}: {}) => { +const SystemMsgModal = () => { const { t } = useTranslation(); const { userInfo, systemMsgReadId, setSysMsgReadId } = useUserStore(); diff --git a/projects/app/src/pageComponents/account/team/EnterpriseAuth/AmountForm.tsx b/projects/app/src/pageComponents/account/team/EnterpriseAuth/AmountForm.tsx new file mode 100644 index 000000000000..531b81e508d3 --- /dev/null +++ b/projects/app/src/pageComponents/account/team/EnterpriseAuth/AmountForm.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { Box, Flex, Input } from '@chakra-ui/react'; +import type { UseFormReturn } from 'react-hook-form'; +import type { TFunction } from 'next-i18next'; +import type { GetEnterpriseAuthCurrentTaskDetailResponseType } from '@fastgpt/global/openapi/support/user/team/enterpriseAuth/api'; +import { enterpriseAuthContactBusinessUrl } from './utils'; +import { + AmountInfoRow, + AmountYuanPattern, + formatBankAccountForDisplay, + formErrorTextStyles, + inputStyles, + type AmountFormType +} from './shared'; + +type EnterpriseAuthAmountFormProps = { + t: TFunction; + amountForm: UseFormReturn; + taskDetail?: GetEnterpriseAuthCurrentTaskDetailResponseType; + shouldShowAmountError: boolean; + setShowAmountError: (show: boolean) => void; +}; + +const EnterpriseAuthAmountForm = ({ + t, + amountForm, + taskDetail, + shouldShowAmountError, + setShowAmountError +}: EnterpriseAuthAmountFormProps) => { + const amountField = amountForm.register('amountYuan', { + required: true, + pattern: AmountYuanPattern + }); + + return ( + + + + + + + + + + + + + + {t('account_team:enterprise_auth_amount_sent_prefix')} + + {t('account_team:enterprise_auth_contact_business')} + + {t('account_team:enterprise_auth_amount_sent_suffix')} + + + + + {t('account_team:enterprise_auth_amount_label')} + + + { + const [yuan = '', ...centParts] = event.target.value + .replace(/[^\d.]/g, '') + .split('.'); + event.target.value = centParts.length + ? `${yuan}.${centParts.join('').slice(0, 2)}` + : yuan; + void amountField.onChange(event); + setShowAmountError(false); + }} + /> + + {t('account_team:enterprise_auth_amount_error_tip')} + + + + + + ); +}; + +export default React.memo(EnterpriseAuthAmountForm); diff --git a/projects/app/src/pageComponents/account/team/EnterpriseAuth/ContactBusinessModal.tsx b/projects/app/src/pageComponents/account/team/EnterpriseAuth/ContactBusinessModal.tsx new file mode 100644 index 000000000000..84b243cf8ef6 --- /dev/null +++ b/projects/app/src/pageComponents/account/team/EnterpriseAuth/ContactBusinessModal.tsx @@ -0,0 +1,50 @@ +import React, { useCallback } from 'react'; +import { Box, Button } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import MyModal from '@fastgpt/web/components/v2/common/MyModal'; +import { enterpriseAuthContactBusinessUrl } from './utils'; +import { enterpriseAuthFooterButtonStyles } from './shared'; + +type EnterpriseAuthContactBusinessModalProps = { + onClose: () => void; +}; + +const EnterpriseAuthContactBusinessModal = ({ + onClose +}: EnterpriseAuthContactBusinessModalProps) => { + const { t } = useTranslation(); + + const openContactBusiness = useCallback(() => { + window.open(enterpriseAuthContactBusinessUrl, '_blank', 'noopener,noreferrer'); + onClose(); + }, [onClose]); + + return ( + + + + + } + > + {t('account_team:enterprise_auth_no_remaining_times_tip')} + + ); +}; + +export default React.memo(EnterpriseAuthContactBusinessModal); diff --git a/projects/app/src/pageComponents/account/team/EnterpriseAuth/InfoForm.tsx b/projects/app/src/pageComponents/account/team/EnterpriseAuth/InfoForm.tsx new file mode 100644 index 000000000000..20d6a17a9b95 --- /dev/null +++ b/projects/app/src/pageComponents/account/team/EnterpriseAuth/InfoForm.tsx @@ -0,0 +1,275 @@ +import React, { useMemo, useState } from 'react'; +import { Box, Button, Flex, Grid, Input, Text, Textarea } from '@chakra-ui/react'; +import { Controller, type UseFormReturn, useWatch } from 'react-hook-form'; +import type { TFunction } from 'next-i18next'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import type { StartEnterpriseAuthBodyType } from '@fastgpt/global/openapi/support/user/team/enterpriseAuth/api'; +import { + isBankAccount, + isUnifiedCreditCode +} from '@fastgpt/global/support/user/team/enterpriseAuth/utils'; +import { + Field, + Section, + fieldRules, + formErrorTextStyles, + invalidInputStyles, + inputStyles, + normalizeBankAccount, + normalizeUnifiedCreditCode, + textareaStyles, + type EnterpriseAuthBankOption +} from './shared'; + +type EnterpriseAuthInfoFormProps = { + t: TFunction; + startForm: UseFormReturn; + bankOptions: EnterpriseAuthBankOption[]; + hasSubmittedStartForm: boolean; + hasBankLoadError: boolean; + isBankLoading: boolean; + reloadBanks: () => void; +}; + +const EnterpriseAuthInfoForm = ({ + t, + startForm, + bankOptions, + hasSubmittedStartForm, + hasBankLoadError, + isBankLoading, + reloadBanks +}: EnterpriseAuthInfoFormProps) => { + const [hasBlurredUnifiedCreditCode, setHasBlurredUnifiedCreditCode] = useState(false); + const [hasBlurredBankAccount, setHasBlurredBankAccount] = useState(false); + const unifiedCreditCode = useWatch({ + control: startForm.control, + name: 'unifiedCreditCode' + }); + const bankAccount = useWatch({ + control: startForm.control, + name: 'bankAccount' + }); + const watchedFields = useWatch({ + control: startForm.control + }); + const unifiedCreditCodeRegister = useMemo( + () => + startForm.register('unifiedCreditCode', { + ...fieldRules.unifiedCreditCode, + setValueAs: normalizeUnifiedCreditCode + }), + [startForm] + ); + const bankAccountRegister = useMemo( + () => + startForm.register('bankAccount', { + ...fieldRules.bankAccount, + setValueAs: normalizeBankAccount + }), + [startForm] + ); + const shouldShowUnifiedCreditCodeError = + !!unifiedCreditCode?.trim() && + (hasBlurredUnifiedCreditCode || startForm.formState.isSubmitted) && + !isUnifiedCreditCode(unifiedCreditCode); + const shouldShowBankAccountError = + !!bankAccount?.trim() && + (hasBlurredBankAccount || startForm.formState.isSubmitted) && + !isBankAccount(bankAccount); + const fieldErrors = startForm.formState.errors; + const isEmptyAfterSubmit = (value?: string) => hasSubmittedStartForm && !value?.trim(); + const shouldShowUnifiedCreditCodeEmptyError = + isEmptyAfterSubmit(unifiedCreditCode) || + (!!fieldErrors.unifiedCreditCode && !unifiedCreditCode?.trim()); + const shouldShowBankAccountEmptyError = + isEmptyAfterSubmit(bankAccount) || (!!fieldErrors.bankAccount && !bankAccount?.trim()); + const shouldShowBankNameEmptyError = + isEmptyAfterSubmit(watchedFields.bankName) || + (!!fieldErrors.bankName && !watchedFields.bankName?.trim()); + + return ( + +
+ + + + + + { + setHasBlurredUnifiedCreditCode(true); + unifiedCreditCodeRegister.onBlur(event); + }} + /> + + + + + + { + setHasBlurredBankAccount(true); + bankAccountRegister.onBlur(event); + }} + /> + + + ( + + value={field.value} + list={bankOptions} + isSearch + isDisabled={hasBankLoadError} + isInvalid={shouldShowBankNameEmptyError} + isLoading={isBankLoading} + placeholder={ + hasBankLoadError + ? t('account_team:enterprise_auth_bank_load_failed') + : bankOptions.length + ? t('account_team:enterprise_auth_bank_name_placeholder') + : isBankLoading + ? t('account_team:enterprise_auth_bank_loading_placeholder') + : t('account_team:enterprise_auth_bank_name_placeholder') + } + opacity={1} + _disabled={{ + opacity: 1, + cursor: 'not-allowed', + bg: 'myWhite.300', + borderColor: 'myGray.100', + color: 'myGray.400' + }} + _hover={ + shouldShowBankNameEmptyError + ? { borderColor: 'red.500' } + : { borderColor: 'primary.300' } + } + size={'sm'} + h={'32px'} + borderColor={shouldShowBankNameEmptyError ? 'red.500' : 'borderColor.low'} + onChange={(value) => { + field.onChange(value); + startForm.clearErrors('bankName'); + }} + /> + )} + /> + {hasBankLoadError && ( + + + {t('account_team:enterprise_auth_bank_load_failed')} + + + + )} + + +
+ + + + + +
+ + + + + + + + + + + +