fix: harden real backend workflows and channel connections
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Copy, Eye, FileText, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { adminApi, type AdminChannel, type ChannelLinkLogResponse } from '@/api/adminApi';
|
||||
import { adminApi, type AdminChannel, type ChannelConnectionLogResponse, type CmppConnectionState } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Modal, Select, Tag, Textarea } from '@/components/ui';
|
||||
|
||||
type Carrier = 'mobile' | 'unicom' | 'telecom' | 'all';
|
||||
@@ -41,7 +41,7 @@ type ChannelConfirmAction = {
|
||||
|
||||
type ChannelLogState = {
|
||||
channel: SmsChannel;
|
||||
data?: ChannelLinkLogResponse;
|
||||
data?: ChannelConnectionLogResponse;
|
||||
};
|
||||
|
||||
const carrierOptions = [
|
||||
@@ -54,10 +54,10 @@ const carrierOptions = [
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '全部状态', value: 'all' },
|
||||
{ label: '链接正常', value: 'normal' },
|
||||
{ label: '连接正常', value: 'normal' },
|
||||
{ label: '已停用', value: 'stopped' },
|
||||
{ label: '链接中', value: 'connecting' },
|
||||
{ label: '链接失败', value: 'failed' },
|
||||
{ label: '连接中', value: 'connecting' },
|
||||
{ label: '连接失败', value: 'failed' },
|
||||
];
|
||||
|
||||
const protocolOptions = [
|
||||
@@ -93,10 +93,10 @@ const carrierToneMap: Record<Carrier, 'info' | 'danger' | 'success' | 'neutral'>
|
||||
};
|
||||
|
||||
const statusLabelMap: Record<ChannelStatus, string> = {
|
||||
normal: '链接正常',
|
||||
normal: '连接正常',
|
||||
stopped: '已停用',
|
||||
connecting: '链接中',
|
||||
failed: '链接失败',
|
||||
connecting: '连接中',
|
||||
failed: '连接失败',
|
||||
};
|
||||
|
||||
const statusToneMap: Record<ChannelStatus, 'success' | 'neutral' | 'info' | 'danger'> = {
|
||||
@@ -106,21 +106,31 @@ const statusToneMap: Record<ChannelStatus, 'success' | 'neutral' | 'info' | 'dan
|
||||
failed: 'danger',
|
||||
};
|
||||
|
||||
function mapApiChannel(channel: AdminChannel): SmsChannel {
|
||||
const statusMap: Record<string, ChannelStatus> = {
|
||||
active: 'normal',
|
||||
disabled: 'stopped',
|
||||
deleted: 'stopped',
|
||||
connecting: 'connecting',
|
||||
failed: 'failed',
|
||||
};
|
||||
function resolveChannelStatus(channel: AdminChannel, connections: CmppConnectionState[] = []): ChannelStatus {
|
||||
if (channel.status !== 'active') {
|
||||
return 'stopped';
|
||||
}
|
||||
if (connections.some((connection) =>
|
||||
connection.status === 'connected'
|
||||
&& connection.currentConnections > 0
|
||||
&& connection.desiredConnections > 0,
|
||||
)) {
|
||||
return 'normal';
|
||||
}
|
||||
if (connections.some((connection) => ['auth_failed', 'heartbeat_timeout', 'failed', 'error'].includes(connection.status) || connection.lastError)) {
|
||||
return 'failed';
|
||||
}
|
||||
return 'connecting';
|
||||
}
|
||||
|
||||
function mapApiChannel(channel: AdminChannel, connections: CmppConnectionState[] = channel.connectionStates ?? []): SmsChannel {
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
carrier: channel.carrier === 'unicom' || channel.carrier === 'telecom' || channel.carrier === 'all' ? channel.carrier : 'mobile',
|
||||
sendRegion: channel.sendRegion ?? '全国',
|
||||
unitPrice: channel.unitPrice,
|
||||
status: statusMap[channel.status] ?? 'normal',
|
||||
status: resolveChannelStatus(channel, connections),
|
||||
total: 0,
|
||||
successRate: 0,
|
||||
successCount: 0,
|
||||
@@ -353,13 +363,21 @@ export function AdminChannelsPage() {
|
||||
const [confirmAction, setConfirmAction] = useState<ChannelConfirmAction | null>(null);
|
||||
const [logState, setLogState] = useState<ChannelLogState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function loadChannels() {
|
||||
adminApi.listChannels()
|
||||
.then((items) => {
|
||||
setChannels(items.filter((item) => item.status !== 'deleted').map(mapApiChannel));
|
||||
.then(async (items) => {
|
||||
const visibleChannels = items.filter((item) => item.status !== 'deleted');
|
||||
const connections = await Promise.all(visibleChannels.map((channel) =>
|
||||
adminApi.listChannelConnections(channel.id).catch(() => [] as CmppConnectionState[]),
|
||||
));
|
||||
setChannels(visibleChannels.map((item, index) => mapApiChannel(item, connections[index])));
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '通道列表加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadChannels();
|
||||
}, []);
|
||||
|
||||
const filteredChannels = useMemo(
|
||||
@@ -375,16 +393,15 @@ export function AdminChannelsPage() {
|
||||
async function upsertChannel(nextChannel: SmsChannel) {
|
||||
try {
|
||||
if (modal?.mode === 'edit' && modal.channel) {
|
||||
const updated = await adminApi.updateChannel(modal.channel.id, buildChannelPayload(nextChannel, nextChannel.passwordCipher));
|
||||
setChannels((items) => items.map((item) => (item.id === updated.id ? mapApiChannel(updated) : item)));
|
||||
await adminApi.updateChannel(modal.channel.id, buildChannelPayload(nextChannel, nextChannel.passwordCipher));
|
||||
} else {
|
||||
const created = await adminApi.createChannel({
|
||||
await adminApi.createChannel({
|
||||
code: `CH-${Date.now()}`,
|
||||
...buildChannelPayload(nextChannel, nextChannel.passwordCipher || 'secret'),
|
||||
status: 'active',
|
||||
});
|
||||
setChannels((items) => [mapApiChannel(created), ...items]);
|
||||
}
|
||||
loadChannels();
|
||||
setModal(null);
|
||||
setError('');
|
||||
} catch (failure) {
|
||||
@@ -393,8 +410,8 @@ export function AdminChannelsPage() {
|
||||
}
|
||||
|
||||
async function toggleChannel(channel: SmsChannel) {
|
||||
const updated = await adminApi.changeChannelStatus(channel.id, mapUiStatusToApi(channel));
|
||||
setChannels((items) => items.map((item) => (item.id === channel.id ? mapApiChannel(updated) : item)));
|
||||
await adminApi.changeChannelStatus(channel.id, mapUiStatusToApi(channel));
|
||||
loadChannels();
|
||||
}
|
||||
|
||||
async function deleteChannel(id: string) {
|
||||
@@ -403,13 +420,13 @@ export function AdminChannelsPage() {
|
||||
}
|
||||
|
||||
async function copyChannel(channel: SmsChannel) {
|
||||
const copied = await adminApi.copyChannel(channel.id);
|
||||
setChannels((items) => [mapApiChannel(copied), ...items]);
|
||||
await adminApi.copyChannel(channel.id);
|
||||
loadChannels();
|
||||
}
|
||||
|
||||
async function openLinkLogs(channel: SmsChannel) {
|
||||
setLogState({ channel });
|
||||
const data = await adminApi.listChannelLinkLogs(channel.id);
|
||||
const data = await adminApi.listChannelConnectionLogs(channel.id);
|
||||
setLogState({ channel, data });
|
||||
}
|
||||
|
||||
@@ -446,7 +463,7 @@ export function AdminChannelsPage() {
|
||||
: confirmAction?.type === 'copy'
|
||||
? '系统将复制当前通道配置和报备详情,并新建一条名称带“副本”的通道。'
|
||||
: confirmAction?.channel.status === 'stopped'
|
||||
? '启用后通道会进入链接中状态,后续可继续观察网关连接。'
|
||||
? '启用后通道会进入连接中状态,后续可继续观察网关连接。'
|
||||
: '停用后该通道将不再承接新的发送任务。';
|
||||
|
||||
return (
|
||||
@@ -491,7 +508,7 @@ export function AdminChannelsPage() {
|
||||
<div className="sms-channel-status-cell">
|
||||
<Tag tone={statusToneMap[channel.status]}>{statusLabelMap[channel.status]}</Tag>
|
||||
<button onClick={() => void openLinkLogs(channel)} type="button">
|
||||
<FileText size={14} />链接日志
|
||||
<FileText size={14} />连接日志
|
||||
</button>
|
||||
</div>
|
||||
<strong className="sms-channel-total">{channel.total.toLocaleString('zh-CN')}</strong>
|
||||
@@ -564,7 +581,7 @@ export function AdminChannelsPage() {
|
||||
onClose={() => setLogState(null)}
|
||||
open
|
||||
size="xl"
|
||||
title={<div className="template-modal-title"><h2>链接日志</h2><p>{logState.channel.name}</p></div>}
|
||||
title={<div className="template-modal-title"><h2>连接日志</h2><p>{logState.channel.name}</p></div>}
|
||||
>
|
||||
<div className="channel-log-list">
|
||||
{(logState.data?.logs ?? []).map((log) => (
|
||||
@@ -579,8 +596,8 @@ export function AdminChannelsPage() {
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{logState.data && logState.data.logs.length === 0 ? <p className="muted">暂无链接日志</p> : null}
|
||||
{!logState.data ? <p className="muted">正在加载链接日志...</p> : null}
|
||||
{logState.data && logState.data.logs.length === 0 ? <p className="muted">暂无连接日志</p> : null}
|
||||
{!logState.data ? <p className="muted">正在加载连接日志...</p> : null}
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user