feat(company): add subscription expiry management and expired company guard

Add validity period fields to company CRUD, display expiry info in
table and profile, and block access for expired companies via a
full-screen guard component.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 16:15:58 +08:00
parent 43d6e9207a
commit fc55965555
10 changed files with 165 additions and 6 deletions

View File

@@ -15,7 +15,8 @@ export default {
'/api': {
// The address to proxy
// target: 'http://2333z061l7.51mypc.cn',
target: 'http://172.20.177.121:8080',
// target: 'http://172.20.177.121:8080',
target: 'http://172.20.177.121:8082',
changeOrigin: true,
pathRewrite: {
'^/api': '',

View File

@@ -17,6 +17,7 @@ import ServerErrorPage from './pages/500';
import { errorConfig } from './requestErrorConfig';
import { getDeviceId } from './utils/fp';
import './utils/sentry';
import CompanyExpireGuard from './components/CompanyExpireGuard';
import GlobalErrorBoundary from './components/GlobalErrorBoundary';
import PermissionGuard from './components/PermissionGuard';
import SSEController from './components/SSEController';
@@ -197,6 +198,12 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
// Add a loading state
childrenRender: (children) => {
if (initialState?.loading) return <PageLoading />;
const { company } = currentUser ?? {};
if (company?.special !== 0 && company?.validityStatus === 2) {
return <CompanyExpireGuard />;
}
return (
<>
{currentUser && (

View File

@@ -0,0 +1,50 @@
import { Button, Result, Space } from 'antd';
import { logoutAndToLoginPage } from '@/access';
import useUserInfo from '@/hooks/useUserInfo';
import { $t } from '@/utils/i18n';
const CompanyExpireGuard: React.FC = () => {
const { userInfo } = useUserInfo();
const isExpired = userInfo?.company?.validityStatus === 2;
if (!isExpired) return null;
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9999,
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Result
status="warning"
title={$t('pages.company.expireTitle')}
subTitle={$t('pages.company.expireSubTitle')}
extra={
<Space>
<Button type="primary" onClick={() => window.location.reload()}>
{$t('pages.refresh')}
</Button>
<Button
onClick={() => {
logoutAndToLoginPage();
}}
>
{$t('pages.logout')}
</Button>
</Space>
}
/>
</div>
);
};
export default CompanyExpireGuard;

View File

@@ -46,6 +46,8 @@ export default {
'table.contactEmail': 'Contact Email',
'table.dataSourceCount': 'Data Sources',
'table.userCount': 'Users',
'table.validityEndTime': 'Expire At (UTC+0)',
'table.validityEndTime.tips': 'Company subscription expiry date ',
'table.createTime': 'Create Time',
'table.name': 'Name',
'table.ipAddress': 'IP Address',
@@ -87,6 +89,13 @@ export default {
'pages.totalAlertCount': 'Total Alerts',
'pages.totalCompanyCount': 'Total Companies',
'pages.activeCompanyCount': 'Active Companies',
'pages.company.expired': 'Expired',
'pages.company.permanent': 'Permanent',
'pages.company.expireTitle': 'Company Subscription Expired',
'pages.company.expireSubTitle':
"Your company's license has expired. All features are temporarily locked. Please contact your sales representative to renew.",
'pages.company.expireInDays': 'Subscription expires in {days} days',
'pages.company.expireToday': 'Expires Today',
'pages.totalDataSourceCount': 'Total Data Sources',
'pages.activeDataSourceCount': 'Active Data Sources',
'pages.mt4DataSourceCount': 'MT4 Data Sources',
@@ -329,6 +338,9 @@ export default {
'form.role.placeholder2': 'System roles cannot modify permission levels',
'form.role.badgeColor': 'Badge Color',
'form.role.menuIds': 'Bound Permissions (Menus)',
'form.company.validityEndTime': 'Expiry At (UTC+0)',
'form.company.validityEndTime.placeholder': 'Select expiry date',
'form.company.validityEndTime.tips': 'Blank for Permanent',
'form.role.permissionGuide': 'Permission Guide',
'form.role.otherPermissions': 'Other Menu Permissions',

View File

@@ -46,6 +46,8 @@ export default {
'table.contactEmail': '联系邮箱',
'table.dataSourceCount': '数据源',
'table.userCount': '用户数',
'table.validityEndTime': '订阅到期时间(UTC+0)',
'table.validityEndTime.tips': '公司订阅服务到期时间',
'table.createTime': '创建时间',
'table.name': '名称',
'table.ipAddress': 'IP地址',
@@ -87,6 +89,13 @@ export default {
'pages.totalAlertCount': '总告警数',
'pages.totalCompanyCount': '总公司数',
'pages.activeCompanyCount': '活跃公司数',
'pages.company.expired': '已过期',
'pages.company.permanent': '永久有效',
'pages.company.expireTitle': '公司订阅已过期',
'pages.company.expireSubTitle':
'您的公司在该系统的使用授权已到期,各项功能已暂时锁定。请联系商务代表进行续费操作以恢复访问。',
'pages.company.expireInDays': '订阅即将到期,剩余 {days}天',
'pages.company.expireToday': '今天到期',
'pages.totalDataSourceCount': '数据源总数',
'pages.activeDataSourceCount': '活跃数据源',
'pages.mt4DataSourceCount': 'MT4 数据源',
@@ -321,6 +330,9 @@ export default {
'form.role.placeholder2': '系统角色权限级别不可修改',
'form.role.badgeColor': '徽章颜色',
'form.role.menuIds': '绑定权限(菜单)',
'form.company.validityEndTime': '到期时间(UTC+0)',
'form.company.validityEndTime.placeholder': '请选择到期时间',
'form.company.validityEndTime.tips': '留空为永久有效',
'form.role.permissionGuide': '权限怎么选',
'form.role.otherPermissions': '其他菜单权限',

View File

@@ -1,5 +1,6 @@
import {
ModalForm,
ProFormDatePicker,
ProFormDependency,
type ProFormInstance,
ProFormSelect,
@@ -8,6 +9,7 @@ import {
} from '@ant-design/pro-components';
import { useAccess } from '@umijs/max';
import { Divider, Space, Typography } from 'antd';
import type { Dayjs } from 'dayjs';
import { useRef } from 'react';
import useUserInfo from '@/hooks/useUserInfo';
import {
@@ -16,6 +18,7 @@ import {
getEmailConfigList,
} from '@/services/api';
import { $t } from '@/utils/i18n';
import { utcInstance as dayjs } from '@/utils/timeFormat';
const { Text } = Typography;
type CompanyListItem = Partial<API.CompanyListItem>;
@@ -25,7 +28,7 @@ export default (props: {
onSubmit: (values: CompanyListItem) => void;
onDone: () => void;
}) => {
const { isGlobalCompany } = useAccess();
const { isGlobalCompany, isSuperAdmin } = useAccess();
const { userInfo } = useUserInfo();
const { modalVisit, formValues, onSubmit, onDone } = props;
@@ -43,6 +46,13 @@ export default (props: {
if (!isGlobalCompany) {
values.companyId = userInfo?.companyId;
}
if (isSuperAdmin) {
values.validityEndTime = values.validityEndTime
? dayjs(values.validityEndTime).endOf('day').format('YYYY-MM-DD')
: '';
values.validityStartTime = values.validityEndTime ? '2026-01-01' : '';
}
return onSubmit(values);
}}
modalProps={{
@@ -70,6 +80,7 @@ export default (props: {
return {
...values,
...companyMailHostConfig,
validityEndTime: values.validityEndTime || undefined,
};
}
return values || {};
@@ -106,6 +117,23 @@ export default (props: {
},
]}
/>
{isSuperAdmin && (
<ProFormDatePicker
hidden={formValues?.id === void 0}
name="validityEndTime"
label={`${$t('form.company.validityEndTime')} (${$t('form.company.validityEndTime.tips')})`}
placeholder={$t('form.company.validityEndTime.placeholder')}
convertValue={(value: string | Dayjs | null | undefined) =>
value ? value : undefined
}
fieldProps={{
style: { width: '100%' },
disabledDate: (current: Dayjs) =>
current?.isBefore(dayjs().startOf('day')),
}}
// extra={<Text type="danger">{$t('pages.company.expired')}</Text>}
/>
)}
{formValues?.id !== void 0 && (
<>

View File

@@ -5,7 +5,8 @@ import {
type ProColumns,
ProTable,
} from '@ant-design/pro-components';
import { Badge, Button, Col, Row } from 'antd';
import { useAccess } from '@umijs/max';
import { Badge, Button, Col, Row, Space, Typography } from 'antd';
import React, { useRef, useState } from 'react';
import HeadStatisticCard, {
type CardInfo,
@@ -13,9 +14,13 @@ import HeadStatisticCard, {
import MyTag from '@/components/MyTag';
import { companyHandle, getCompanyPages } from '@/services/api';
import { $t } from '@/utils/i18n';
import { utcInstance as dayjs } from '@/utils/timeFormat';
import PopUp from './Popup';
const { Text } = Typography;
const Company: React.FC = () => {
const { isSuperAdmin } = useAccess();
const [modalVisit, setModalVisit] = useState(false);
const [cardInfo, setCardInfo] = useState({
@@ -92,6 +97,28 @@ const Company: React.FC = () => {
);
},
},
{
title: $t('table.validityEndTime'),
dataIndex: 'validityEndTime',
tooltip: $t('table.validityEndTime.tips'),
hideInTable: !isSuperAdmin,
render: (_, { validityEndTime, validityStatus }) => {
return (
<Space>
{!validityEndTime ? (
<Text type="secondary">{$t('pages.company.permanent')}</Text>
) : (
<span>{dayjs(validityEndTime).format('YYYY-MM-DD')}</span>
)}
{validityStatus === 2 && (
<Text type="danger" style={{ fontSize: 11 }}>
{$t('pages.company.expired')}
</Text>
)}
</Space>
);
},
},
{
title: $t('table.createTime'),
dataIndex: 'createTime',
@@ -122,7 +149,10 @@ const Company: React.FC = () => {
size="small"
key="edit"
onClick={() => {
setFormValues(item);
setFormValues({
...item,
validityEndTime: item.validityEndTime || undefined,
});
setModalVisit(true);
}}
>

View File

@@ -3,7 +3,7 @@ import {
ProForm,
ProFormText,
} from '@ant-design/pro-components';
import { history } from '@umijs/max';
import { history, useAccess } from '@umijs/max';
import {
App,
Button,
@@ -15,7 +15,7 @@ import {
Space,
Tag,
} from 'antd';
import React from 'react';
import React, { useEffect } from 'react';
import { clearAccessToken } from '@/access';
import PasswordLevel from '@/components/PasswordLevel';
import useUserInfo from '@/hooks/useUserInfo';
@@ -24,6 +24,7 @@ import { $t } from '@/utils/i18n';
const Profile: React.FC = () => {
const { userInfo, setUserInfo } = useUserInfo();
const { isGlobalCompany } = useAccess();
const userCompany = userInfo?.company;
const { message: antdMessage, modal } = App.useApp();
@@ -71,6 +72,19 @@ const Profile: React.FC = () => {
},
];
useEffect(() => {
if (!isGlobalCompany) {
items.push({
key: '6',
span: 'filled',
label: $t('pages.company.permanent'),
children: userCompany?.validityEndTime
? userCompany.validityEndTime
: $t('pages.company.permanent'),
});
}
}, [userInfo]);
const changePasswordSuccessHadle = async () => {
let countdown = 5;
let timer: NodeJS.Timeout | null = null;

View File

@@ -83,6 +83,8 @@ export async function companyHandle(
name?: string;
email?: string;
status?: number;
validityStartTime?: string | null;
validityEndTime?: string | null;
},
options?: ObjType,
) {

View File

@@ -113,6 +113,9 @@ declare namespace API {
fromName: string | null;
mailHostConfigId: number | string | null;
mailStatus: number | null;
validityStartTime?: string | null;
validityEndTime?: string | null;
validityStatus?: number | null;
};
// "DataSource" Type