fix: complete first version issue remediation
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 { 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}
|
||||
|
||||
Reference in New Issue
Block a user