fix: harden real backend admin workflows and ui

This commit is contained in:
hectorzhao
2026-07-03 19:29:56 +08:00
parent dd09d91c1e
commit 8cca361441
71 changed files with 5111 additions and 4439 deletions
+145 -35
View File
@@ -1,8 +1,16 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ArrowLeft } from 'lucide-react';
import { adminApi, type ChannelGroup } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Select } from '@/components/ui';
import { ArrowLeft, RadioTower } from 'lucide-react';
import { adminApi, type ChannelGroup, type DictionaryItem, type EnterpriseApplication } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Select, Tag } from '@/components/ui';
type Carrier = 'mobile' | 'unicom' | 'telecom';
const carrierMeta: Record<Carrier, { label: string; description: string }> = {
mobile: { label: '移动', description: '移动号码只会进入移动通道组' },
unicom: { label: '联通', description: '联通号码只会进入联通通道组' },
telecom: { label: '电信', description: '电信号码只会进入电信通道组' },
};
export function AdminSmsApplicationFormPage() {
const navigate = useNavigate();
@@ -20,70 +28,128 @@ export function AdminSmsApplicationFormPage() {
const [unicomGroupId, setUnicomGroupId] = useState('');
const [telecomGroupId, setTelecomGroupId] = useState('');
const [error, setError] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
adminApi.listChannelGroups()
.then((items) => setGroups(items.filter((item) => item.status !== 'disabled' && item.status !== 'deleted')))
.catch((failure: Error) => setError(failure.message || '通道组加载失败'));
}, []);
let cancelled = false;
async function loadForm() {
try {
const [groupItems, application, routeRules] = await Promise.all([
adminApi.listChannelGroups(),
isEdit && appId ? adminApi.getEnterpriseApplication(appId) : Promise.resolve<EnterpriseApplication | null>(null),
isEdit ? adminApi.listChannelRouteRules() : Promise.resolve<DictionaryItem[]>([]),
]);
if (cancelled) {
return;
}
setGroups(groupItems.filter((item) => item.status !== 'disabled' && item.status !== 'deleted'));
if (application) {
if (enterpriseId && application.tenantId !== enterpriseId) {
setError('应用不属于当前企业,已停止加载');
return;
}
hydrateApplication(application, routeRules);
}
} catch (failure) {
if (!cancelled) {
setError(failure instanceof Error ? failure.message : '短信应用加载失败');
}
}
}
void loadForm();
return () => {
cancelled = true;
};
}, [appId, enterpriseId, isEdit]);
function goBack() {
navigate('/admin/enterprise-applications');
}
function submit() {
function hydrateApplication(application: EnterpriseApplication, routeRules: DictionaryItem[]) {
setAppName(application.name);
setScene(application.scene ?? '');
setDailyLimit(application.dailyLimit ? String(application.dailyLimit) : '');
setCustomerUnitPrice(((application.customerUnitPrice ?? 0) / 100).toFixed(4));
setPhoneDailyLimit(application.maxPhonesPerTask ? String(application.maxPhonesPerTask) : '');
setMismatchPolicy(application.templateMismatchMode ?? 'reject');
setIpAddress(application.ipAllowlist?.map((item) => item.ipCidr).join('\n') ?? '');
const activeRules = routeRules.filter((rule) => (
rule.applicationId === application.id
&& rule.status !== 'deleted'
&& !rule.province
&& !rule.channelId
));
setMobileGroupId(getRouteGroupId(activeRules, 'mobile'));
setUnicomGroupId(getRouteGroupId(activeRules, 'unicom'));
setTelecomGroupId(getRouteGroupId(activeRules, 'telecom'));
}
async function submit() {
if (!enterpriseId) {
setError('缺少企业 ID');
return;
}
if (isEdit) {
setError('短信应用编辑接口待补,当前不做本地模拟保存');
return;
}
const selectedGroups = [
{ carrier: 'mobile', groupId: mobileGroupId },
{ carrier: 'unicom', groupId: unicomGroupId },
{ carrier: 'telecom', groupId: telecomGroupId },
{ carrier: 'mobile' as Carrier, groupId: mobileGroupId },
{ carrier: 'unicom' as Carrier, groupId: unicomGroupId },
{ carrier: 'telecom' as Carrier, groupId: telecomGroupId },
].filter((item) => item.groupId);
if (selectedGroups.length === 0) {
setError('请至少配置一个运营商通道组');
return;
}
adminApi.createEnterpriseApplication({
tenantId: enterpriseId,
const payload = {
name: appName,
scene,
dailyLimit: Number(dailyLimit) || undefined,
customerUnitPrice: Math.round(Number(customerUnitPrice || 0) * 100),
maxPhonesPerTask: Number(phoneDailyLimit) || undefined,
templateMismatchMode: mismatchPolicy,
ipAllowlist: ipAddress ? [ipAddress] : [],
})
.then(async (application) => {
await Promise.all(selectedGroups.map((item, index) => adminApi.createChannelRouteRule({
tenantId: enterpriseId,
applicationId: application.id,
groupId: item.groupId,
ipAllowlist: parseIpAllowlist(ipAddress),
};
setSaving(true);
setError('');
try {
const application = isEdit && appId
? await adminApi.updateEnterpriseApplication(appId, payload)
: await adminApi.createEnterpriseApplication({ tenantId: enterpriseId, ...payload });
await adminApi.replaceApplicationRouteRules(application.id, {
routes: selectedGroups.map((item, index) => ({
carrier: item.carrier,
groupId: item.groupId,
priority: (index + 1) * 10,
status: 'active',
})));
goBack();
})
.catch((failure: Error) => setError(failure.message || '短信应用保存失败'));
})),
});
goBack();
} catch (failure) {
setError(failure instanceof Error ? failure.message : '短信应用保存失败');
} finally {
setSaving(false);
}
}
const groupOptionsByCarrier = (carrier: ChannelGroup['carrier']) => [
{ label: '不配置', value: '' },
...groups.filter((group) => group.carrier === carrier).map((group) => ({ label: group.name, value: group.id })),
];
const selectedGroupCount = [mobileGroupId, unicomGroupId, telecomGroupId].filter(Boolean).length;
const routeCards: Array<{ carrier: Carrier; groupId: string; onChange: (value: string) => void }> = [
{ carrier: 'mobile', groupId: mobileGroupId, onChange: setMobileGroupId },
{ carrier: 'unicom', groupId: unicomGroupId, onChange: setUnicomGroupId },
{ carrier: 'telecom', groupId: telecomGroupId, onChange: setTelecomGroupId },
];
return (
<section className="page-stack admin-app-form-page">
<div className="page-heading">
<div>
<Breadcrumb items={[isEdit ? '编辑短信应用' : '添加短信应用']} />
<p></p>
<p></p>
</div>
<Button icon={<ArrowLeft size={16} />} onClick={goBack} variant="ghost"></Button>
</div>
@@ -91,7 +157,7 @@ export function AdminSmsApplicationFormPage() {
<div className="surface admin-app-form-card">
<section className="ui-detail-section">
<div className="ui-detail-section__header"><h3></h3></div>
<div className="ui-detail-section__header"><h3></h3><p></p></div>
<div className="admin-app-form-grid">
<Input label="应用名称" onChange={(event) => setAppName(event.target.value)} placeholder="请输入应用名称" required value={appName} />
<Input label="应用场景" onChange={(event) => setScene(event.target.value)} placeholder="行业通知/营销推广/验证码" value={scene} />
@@ -109,18 +175,62 @@ export function AdminSmsApplicationFormPage() {
required
value={mismatchPolicy}
/>
<Input label="IP 白名单" onChange={(event) => setIpAddress(event.target.value)} placeholder="例如 192.168.1.100/32" value={ipAddress} />
<Select label="移动通道组" onChange={(event) => setMobileGroupId(event.target.value)} options={groupOptionsByCarrier('mobile')} value={mobileGroupId} />
<Select label="联通通道组" onChange={(event) => setUnicomGroupId(event.target.value)} options={groupOptionsByCarrier('unicom')} value={unicomGroupId} />
<Select label="电信通道组" onChange={(event) => setTelecomGroupId(event.target.value)} options={groupOptionsByCarrier('telecom')} value={telecomGroupId} />
<Input label="IP 白名单" onChange={(event) => setIpAddress(event.target.value)} placeholder="多个 IP/CIDR 可用逗号、空格或换行分隔" value={ipAddress} />
</div>
</section>
<section className="ui-detail-section">
<div className="ui-detail-section__header">
<div>
<h3></h3>
<p></p>
</div>
<Tag tone={selectedGroupCount > 0 ? 'success' : 'warning'}>{selectedGroupCount}/3 </Tag>
</div>
<div className="admin-app-route-grid">
{routeCards.map((card) => {
const available = groups.filter((group) => group.carrier === card.carrier);
const meta = carrierMeta[card.carrier];
return (
<div className={['admin-app-route-card', card.groupId ? 'is-selected' : ''].filter(Boolean).join(' ')} key={card.carrier}>
<header>
<span><RadioTower size={18} /></span>
<div>
<strong>{meta.label}</strong>
<small>{meta.description}</small>
</div>
<Tag tone={card.groupId ? 'success' : available.length ? 'neutral' : 'warning'}>{card.groupId ? '已选择' : `${available.length} 个可选`}</Tag>
</header>
<Select
label={`${meta.label}通道组`}
onChange={(event) => card.onChange(event.target.value)}
options={groupOptionsByCarrier(card.carrier)}
value={card.groupId}
/>
{!available.length ? <p>{meta.label}</p> : null}
</div>
);
})}
</div>
</section>
<div className="enterprise-form-footer">
<Button disabled={!appName || isEdit} onClick={submit}>{isEdit ? '编辑待补接口' : '创建应用'}</Button>
<Button disabled={!appName || selectedGroupCount === 0 || saving} onClick={() => { void submit(); }}>{saving ? '保存中...' : isEdit ? '保存应用' : '创建应用'}</Button>
<Button onClick={goBack} variant="ghost"></Button>
</div>
</div>
</section>
);
}
function getRouteGroupId(routeRules: DictionaryItem[], carrier: Carrier) {
const rule = routeRules.find((item) => item.carrier === carrier);
return typeof rule?.groupId === 'string' ? rule.groupId : '';
}
function parseIpAllowlist(value: string) {
return value
.split(/[\s,]+/)
.map((item) => item.trim())
.filter(Boolean);
}