fix: implement channel group routing failover

This commit is contained in:
hectorzhao
2026-07-03 11:07:43 +08:00
parent 131f344ac4
commit a890b03163
16 changed files with 853 additions and 121 deletions
+39 -14
View File
@@ -4,13 +4,14 @@ import { useNavigate } from 'react-router-dom';
import { adminApi, type AdminChannel, type ChannelLinkLogResponse } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
type Carrier = 'mobile' | 'unicom' | 'telecom';
type Carrier = 'mobile' | 'unicom' | 'telecom' | 'all';
type ChannelStatus = 'normal' | 'stopped' | 'connecting' | 'failed';
type SmsChannel = {
id: string;
name: string;
carrier: Carrier;
sendRegion: string;
unitPrice: number;
status: ChannelStatus;
total: number;
@@ -47,6 +48,7 @@ const carrierOptions = [
{ label: '移动', value: 'mobile' },
{ label: '联通', value: 'unicom' },
{ label: '电信', value: 'telecom' },
{ label: '三网', value: 'all' },
];
const statusOptions = [
@@ -65,9 +67,7 @@ const protocolOptions = [
const regionOptions = [
{ label: '全国', value: '全国' },
{ label: '华东', value: '华东' },
{ label: '华南', value: '华南' },
{ label: '华北', value: '华北' },
...'北京,天津,河北,山西,内蒙古,辽宁,吉林,黑龙江,上海,江苏,浙江,安徽,福建,江西,山东,河南,湖北,湖南,广东,广西,海南,重庆,四川,贵州,云南,西藏,陕西,甘肃,青海,宁夏,新疆,香港,澳门,台湾'.split(',').map((province) => ({ label: province, value: province })),
];
const extensionOptions = [
@@ -81,12 +81,14 @@ const carrierLabelMap: Record<Carrier, string> = {
mobile: '移动',
unicom: '联通',
telecom: '电信',
all: '三网',
};
const carrierToneMap: Record<Carrier, 'info' | 'danger' | 'success'> = {
const carrierToneMap: Record<Carrier, 'info' | 'danger' | 'success' | 'neutral'> = {
mobile: 'info',
unicom: 'danger',
telecom: 'success',
all: 'neutral',
};
const statusLabelMap: Record<ChannelStatus, string> = {
@@ -114,7 +116,8 @@ function mapApiChannel(channel: AdminChannel): SmsChannel {
return {
id: channel.id,
name: channel.name,
carrier: channel.carrier === 'unicom' || channel.carrier === 'telecom' ? channel.carrier : 'mobile',
carrier: channel.carrier === 'unicom' || channel.carrier === 'telecom' || channel.carrier === 'all' ? channel.carrier : 'mobile',
sendRegion: channel.sendRegion ?? '全国',
unitPrice: channel.unitPrice,
status: statusMap[channel.status] ?? 'normal',
total: 0,
@@ -159,7 +162,7 @@ function ChannelFormModal({
const [name, setName] = useState(channel?.name ?? '');
const [carrier, setCarrier] = useState<Carrier>(channel?.carrier ?? 'mobile');
const [unitPrice, setUnitPrice] = useState(channel ? String(channel.unitPrice / 100) : '0.0300');
const [region, setRegion] = useState('全国');
const [region, setRegion] = useState(channel?.sendRegion ?? '全国');
const [protocol, setProtocol] = useState('CMPP');
const [gatewayHost, setGatewayHost] = useState(channel?.gatewayHost ?? '');
const [gatewayPort, setGatewayPort] = useState(channel?.gatewayPort ?? '17890');
@@ -175,6 +178,7 @@ function ChannelFormModal({
id: channel?.id ?? String(Math.floor(10000 + Math.random() * 80000)),
name: name || '新建短信通道',
carrier,
sendRegion: region,
unitPrice: Number(unitPrice || 0) * 100,
status: channel?.status ?? 'connecting',
total: channel?.total ?? 0,
@@ -212,7 +216,7 @@ function ChannelFormModal({
<Input label="* 通道名称" onChange={(event) => setName(event.target.value)} placeholder="请输入通道名称" value={name} />
<div className="sms-channel-radio-row">
<span>* </span>
{(['mobile', 'unicom', 'telecom'] as const).map((item) => (
{(['mobile', 'unicom', 'telecom', 'all'] as const).map((item) => (
<label key={item}>
<input checked={carrier === item} onChange={() => setCarrier(item)} type="radio" />
{carrierLabelMap[item]}
@@ -350,12 +354,33 @@ export function AdminChannelsPage() {
[carrier, channels, keyword, status],
);
function upsertChannel(nextChannel: SmsChannel) {
setChannels((items) => {
const exists = items.some((item) => item.id === nextChannel.id);
return exists ? items.map((item) => (item.id === nextChannel.id ? nextChannel : item)) : [nextChannel, ...items];
});
setModal(null);
async function upsertChannel(nextChannel: SmsChannel) {
if (modal?.mode === 'edit') {
setError('短信通道编辑接口待补,当前不做本地模拟保存');
return;
}
try {
const created = await adminApi.createChannel({
code: `CH-${Date.now()}`,
name: nextChannel.name,
carrier: nextChannel.carrier,
sendRegion: nextChannel.sendRegion,
gatewayHost: nextChannel.gatewayHost,
gatewayPort: Number(nextChannel.gatewayPort),
enterpriseCode: nextChannel.corpCode,
account: nextChannel.account,
passwordCipher: 'secret',
srcId: nextChannel.accessNo,
rateLimitPerSecond: 100,
unitPrice: Math.round(nextChannel.unitPrice),
status: 'active',
});
setChannels((items) => [mapApiChannel(created), ...items]);
setModal(null);
setError('');
} catch (failure) {
setError(failure instanceof Error ? failure.message : '通道创建失败');
}
}
async function toggleChannel(channel: SmsChannel) {