fix: complete first version issue remediation

This commit is contained in:
hectorzhao
2026-07-11 10:14:37 +08:00
parent 709ac97764
commit 208a6c23f8
73 changed files with 1549 additions and 245 deletions
+117 -23
View File
@@ -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 { CheckCircle2, Copy, Eye, ExternalLink, FileText, Info, Pencil, Plus, Power, Search, Send, Trash2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { adminApi, type AdminChannel, type ChannelConnectionLogResponse, type CmppConnectionState } from '@/api/adminApi';
import { adminApi, type AdminChannel, type ChannelConnectionLogResponse, type ChannelTestResponse, type CmppConnectionState } from '@/api/adminApi';
import { Breadcrumb, Button, Input, Modal, Pagination, Select, Tag, Textarea } from '@/components/ui';
type Carrier = 'mobile' | 'unicom' | 'telecom' | 'all';
@@ -29,6 +29,8 @@ type SmsChannel = {
cmppVersion: '2.0' | '3.0';
desiredConnections: number;
windowSize: number;
extensionDigits: number;
rateLimitPerSecond: number;
passwordCipher?: string;
};
@@ -47,6 +49,22 @@ type ChannelLogState = {
data?: ChannelConnectionLogResponse;
};
const connectionStatusLabelMap: Record<string, string> = {
connected: '已连接',
connecting: '连接中',
reconnecting: '重连中',
disconnected: '已断开',
failed: '连接失败',
auth_failed: '鉴权失败',
heartbeat_timeout: '心跳超时',
};
function formatLogDetail(detail?: unknown) {
if (!detail) return '无附加信息';
if (typeof detail === 'string') return detail;
return JSON.stringify(detail, null, 2);
}
const carrierOptions = [
{ label: '全部运营商', value: 'all' },
{ label: '移动', value: 'mobile' },
@@ -154,6 +172,8 @@ function mapApiChannel(channel: AdminChannel, connections: CmppConnectionState[]
cmppVersion: channel.cmppVersion === '3.0' ? '3.0' : '2.0',
desiredConnections: Number(channel.config?.desiredConnections ?? 1),
windowSize: Number(channel.config?.windowSize ?? 16),
extensionDigits: Number(channel.config?.extensionDigits ?? 0),
rateLimitPerSecond: channel.rateLimitPerSecond,
};
}
@@ -173,10 +193,11 @@ function buildChannelPayload(channel: SmsChannel, passwordCipher?: string) {
passwordCipher: passwordCipher || undefined,
srcId: channel.accessNo,
cmppVersion: channel.cmppVersion,
rateLimitPerSecond: 100,
rateLimitPerSecond: channel.rateLimitPerSecond,
unitPrice: Math.round(channel.unitPrice),
desiredConnections: channel.desiredConnections,
windowSize: channel.windowSize,
config: { extensionDigits: channel.extensionDigits },
};
}
@@ -212,8 +233,8 @@ function ChannelFormModal({
const [cmppVersion, setCmppVersion] = useState<'2.0' | '3.0'>(channel?.cmppVersion ?? '2.0');
const [password, setPassword] = useState('');
const [accessNo, setAccessNo] = useState(channel?.accessNo ?? '');
const [extensionDigits, setExtensionDigits] = useState('0');
const [flowLimit, setFlowLimit] = useState('1-2000');
const [extensionDigits, setExtensionDigits] = useState(String(channel?.extensionDigits ?? 0));
const [flowLimit, setFlowLimit] = useState(String(channel?.rateLimitPerSecond ?? 100));
const [desiredConnections, setDesiredConnections] = useState(String(channel?.desiredConnections ?? 1));
const [windowSize, setWindowSize] = useState(String(channel?.windowSize ?? 16));
@@ -240,6 +261,8 @@ function ChannelFormModal({
cmppVersion,
desiredConnections: Number(desiredConnections) || 1,
windowSize: Number(windowSize) || 16,
extensionDigits: Number(extensionDigits),
rateLimitPerSecond: Number(flowLimit),
passwordCipher: password || undefined,
});
}
@@ -287,12 +310,20 @@ function ChannelFormModal({
<Input label="* 企业代码" onChange={(event) => setCorpCode(event.target.value)} placeholder="请输入企业代码" value={corpCode} />
<Input label="* 网关账号" onChange={(event) => setAccount(event.target.value)} placeholder="请输入网关账号" value={account} />
<Select label="* CMPP版本" onChange={(event) => setCmppVersion(event.target.value as '2.0' | '3.0')} options={cmppVersionOptions} value={cmppVersion} />
<Input label="* 网关密码" onChange={(event) => setPassword(event.target.value)} placeholder="请输入网关密码" type="password" value={password} />
<Input
hint={modal.mode === 'edit' ? '已配置的密码不会回显;留空保持不变,填写新密码才更新。' : undefined}
label="网关密码"
onChange={(event) => setPassword(event.target.value)}
placeholder={modal.mode === 'edit' ? '留空不修改' : '请输入网关密码'}
required={modal.mode === 'create'}
type="password"
value={password}
/>
<div className="sms-channel-inline-field">
<Input label="* 接入号" onChange={(event) => setAccessNo(event.target.value)} placeholder="请输入通道接入号" value={accessNo} />
<Select label="拓展位数" onChange={(event) => setExtensionDigits(event.target.value)} options={extensionOptions} value={extensionDigits} />
</div>
<Input label="* 通道流速" onChange={(event) => setFlowLimit(event.target.value)} suffix="条/秒" value={flowLimit} />
<Select label="拓展位数" onChange={(event) => setExtensionDigits(event.target.value)} options={extensionOptions} value={extensionDigits} />
</div>
<Input label="* 通道流速" max="2000" min="1" onChange={(event) => setFlowLimit(event.target.value)} suffix="条/秒" type="number" value={flowLimit} />
<Input label="* 期望连接数" onChange={(event) => setDesiredConnections(event.target.value)} placeholder="1" value={desiredConnections} />
<Input label="* 提交窗口" onChange={(event) => setWindowSize(event.target.value)} placeholder="16" value={windowSize} />
</div>
@@ -305,16 +336,18 @@ function ChannelFormModal({
function SmsTestModal({
channel,
onClose,
onOpenRecords,
}: {
channel: SmsChannel;
onClose: () => void;
onOpenRecords: () => void;
}) {
const [phones, setPhones] = useState('');
const [content, setContent] = useState('');
const [accessNo, setAccessNo] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const [result, setResult] = useState('');
const [result, setResult] = useState<ChannelTestResponse | null>(null);
const billingCount = Math.max(1, Math.ceil(content.length / 67));
async function submitTestSms() {
@@ -328,14 +361,14 @@ function SmsTestModal({
}
setSubmitting(true);
setError('');
setResult('');
setResult(null);
try {
const response = await adminApi.testChannel(channel.id, {
phones,
content,
accessNo: accessNo.trim() || undefined,
});
setResult(`已提交 ${response.submitted} 条测试短信,测试流水号 ${response.testNo}`);
setResult(response);
} catch (failure) {
setError(failure instanceof Error ? failure.message : '测试短信发送失败');
} finally {
@@ -348,8 +381,9 @@ function SmsTestModal({
footer={(
<>
<Button onClick={onClose} variant="ghost"></Button>
<Button disabled={submitting} icon={<Send size={16} />} onClick={submitTestSms}>
{submitting ? '发送中...' : '发送测试'}
{result ? <Button icon={<ExternalLink size={16} />} onClick={onOpenRecords} variant="ghost"></Button> : null}
<Button disabled={submitting || Boolean(result)} icon={<Send size={16} />} onClick={submitTestSms}>
{submitting ? '发送中...' : result ? '已提交' : '发送测试'}
</Button>
</>
)}
@@ -402,9 +436,22 @@ function SmsTestModal({
</div>
{error ? <p className="form-error">{error}</p> : null}
{result ? (
<div className="signature-alert sms-test-note">
<Info size={18} />
<span>{result}</span>
<div className="sms-test-result">
<div className="sms-test-result__summary">
<CheckCircle2 size={20} />
<div>
<strong></strong>
<span>{result.testNo} {result.submitted} </span>
</div>
</div>
<div className="sms-test-result__records">
{result.messages.map((message) => (
<div key={message.messageRecordId}>
<span>{message.phoneNumber}</span>
<code>{message.submitId}</code>
</div>
))}
</div>
</div>
) : null}
</div>
@@ -423,6 +470,7 @@ export function AdminChannelsPage() {
const [testChannel, setTestChannel] = useState<SmsChannel | null>(null);
const [confirmAction, setConfirmAction] = useState<ChannelConfirmAction | null>(null);
const [logState, setLogState] = useState<ChannelLogState | null>(null);
const [logKeyword, setLogKeyword] = useState('');
const [page, setPage] = useState(1);
const pageSize = 10;
@@ -496,8 +544,14 @@ export function AdminChannelsPage() {
async function openLinkLogs(channel: SmsChannel) {
setLogState({ channel });
const data = await adminApi.listChannelConnectionLogs(channel.id);
setLogState({ channel, data });
setLogKeyword('');
try {
const data = await adminApi.listChannelConnectionLogs(channel.id);
setLogState({ channel, data });
} catch (failure) {
setLogState(null);
setError(failure instanceof Error ? failure.message : '连接日志加载失败');
}
}
function submitConfirmAction() {
@@ -606,6 +660,8 @@ export function AdminChannelsPage() {
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
page={currentPage}
totalPages={totalPages}
onPageChange={setPage}
previousDisabled={currentPage <= 1}
total={filteredChannels.length}
/>
@@ -623,6 +679,10 @@ export function AdminChannelsPage() {
<SmsTestModal
channel={testChannel}
onClose={() => setTestChannel(null)}
onOpenRecords={() => {
setTestChannel(null);
navigate('/admin/sms-records');
}}
/>
) : null}
@@ -654,8 +714,38 @@ export function AdminChannelsPage() {
size="xl"
title={<div className="template-modal-title"><h2></h2><p>{logState.channel.name}</p></div>}
>
<div className="channel-log-list">
{(logState.data?.logs ?? []).map((log) => (
<div className="channel-log-modal">
{logState.data ? (
<div className="channel-connection-summary">
{logState.data.connectionStates.map((connection) => (
<article key={connection.id}>
<div>
<span> ID</span>
<strong>{connection.connectionId}</strong>
</div>
<Tag tone={connection.status === 'connected' ? 'success' : connection.lastError ? 'danger' : 'info'}>
{connectionStatusLabelMap[connection.status] ?? connection.status}
</Tag>
<div>
<span> / </span>
<strong>{connection.currentConnections} / {connection.desiredConnections}</strong>
</div>
<div>
<span></span>
<strong>{connection.lastHeartbeatAt ? new Date(connection.lastHeartbeatAt).toLocaleString('zh-CN', { hour12: false }) : '-'}</strong>
</div>
{connection.lastError ? <p>{connection.lastError}</p> : null}
</article>
))}
{logState.data.connectionStates.length === 0 ? <p className="muted"></p> : null}
</div>
) : null}
<Input label="筛选日志" onChange={(event) => setLogKeyword(event.target.value)} placeholder="事件、资源或详情关键词" value={logKeyword} />
<div className="channel-log-list">
{(logState.data?.logs ?? []).filter((log) => {
const keyword = logKeyword.trim().toLowerCase();
return !keyword || `${log.event} ${log.resourceId ?? ''} ${formatLogDetail(log.detail)}`.toLowerCase().includes(keyword);
}).map((log) => (
<article className="channel-log-item" key={log.id}>
<div>
<strong>{log.event}</strong>
@@ -663,12 +753,16 @@ export function AdminChannelsPage() {
</div>
<div>
<span>{log.resourceId}</span>
<p>{typeof log.detail === 'string' ? log.detail : JSON.stringify(log.detail ?? {})}</p>
<pre>{formatLogDetail(log.detail)}</pre>
</div>
</article>
))}
{logState.data && logState.data.logs.length === 0 ? <p className="muted"></p> : null}
{logState.data && logState.data.logs.filter((log) => {
const keyword = logKeyword.trim().toLowerCase();
return !keyword || `${log.event} ${log.resourceId ?? ''} ${formatLogDetail(log.detail)}`.toLowerCase().includes(keyword);
}).length === 0 ? <p className="muted"></p> : null}
{!logState.data ? <p className="muted">...</p> : null}
</div>
</div>
</Modal>
) : null}