feat: improve channel resilience and operations

This commit is contained in:
hectorzhao
2026-07-24 08:17:33 +08:00
parent 2f781ebb8a
commit afd3c96070
43 changed files with 1969 additions and 299 deletions
+25 -10
View File
@@ -25,12 +25,15 @@ type SmsChannel = {
failureCount: number;
gatewayHost: string;
gatewayPort: string;
businessCode: string;
corpCode: string;
account: string;
accessNo: string;
cmppVersion: '2.0' | '3.0';
desiredConnections: number;
windowSize: number;
heartbeatIntervalSeconds: number;
heartbeatMissThreshold: number;
extensionDigits: number;
rateLimitPerSecond: number;
passwordCipher?: string;
@@ -83,12 +86,6 @@ const statusOptions = [
{ label: '连接失败', value: 'failed' },
];
const protocolOptions = [
{ label: 'CMPP', value: 'CMPP' },
{ label: 'HTTP', value: 'HTTP' },
{ label: 'SGIP', value: 'SGIP' },
];
const cmppVersionOptions = [
{ label: 'CMPP 2.0', value: '2.0' },
{ label: 'CMPP 3.0', value: '3.0' },
@@ -161,12 +158,15 @@ function mapApiChannel(channel: AdminChannel, connections: CmppConnectionState[]
failureCount: 0,
gatewayHost: channel.gatewayHost,
gatewayPort: String(channel.gatewayPort),
businessCode: String(channel.config?.serviceId ?? 'SMS'),
corpCode: channel.enterpriseCode ?? channel.code,
account: channel.account,
accessNo: channel.srcId,
cmppVersion: channel.cmppVersion === '3.0' ? '3.0' : '2.0',
desiredConnections: Number(channel.config?.desiredConnections ?? 1),
windowSize: Number(channel.config?.windowSize ?? 16),
heartbeatIntervalSeconds: Number(channel.config?.heartbeatIntervalSeconds ?? 30),
heartbeatMissThreshold: Number(channel.config?.heartbeatMissThreshold ?? 3),
extensionDigits: Number(channel.config?.extensionDigits ?? 0),
rateLimitPerSecond: channel.rateLimitPerSecond,
};
@@ -183,6 +183,7 @@ function buildChannelPayload(channel: SmsChannel, passwordCipher?: string) {
sendRegion: channel.sendRegion,
gatewayHost: channel.gatewayHost,
gatewayPort: Number(channel.gatewayPort),
protocol: 'CMPP',
enterpriseCode: channel.corpCode,
account: channel.account,
passwordCipher: passwordCipher || undefined,
@@ -192,7 +193,9 @@ function buildChannelPayload(channel: SmsChannel, passwordCipher?: string) {
unitPrice: Math.round(channel.unitPrice),
desiredConnections: channel.desiredConnections,
windowSize: channel.windowSize,
config: { extensionDigits: channel.extensionDigits },
heartbeatIntervalSeconds: channel.heartbeatIntervalSeconds,
heartbeatMissThreshold: channel.heartbeatMissThreshold,
config: { extensionDigits: channel.extensionDigits, serviceId: channel.businessCode },
};
}
@@ -221,9 +224,9 @@ function ChannelFormModal({
const [unitPrice, setUnitPrice] = useState(channel ? moneyUnitsToYuan(channel.unitPrice).toFixed(4) : '0.0300');
const [unitPriceError, setUnitPriceError] = 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');
const [gatewayPort, setGatewayPort] = useState(channel?.gatewayPort ?? '7890');
const [businessCode, setBusinessCode] = useState(channel?.businessCode ?? 'SMS');
const [corpCode, setCorpCode] = useState(channel?.corpCode ?? '');
const [account, setAccount] = useState(channel?.account ?? '');
const [cmppVersion, setCmppVersion] = useState<'2.0' | '3.0'>(channel?.cmppVersion ?? '2.0');
@@ -233,6 +236,8 @@ function ChannelFormModal({
const [flowLimit, setFlowLimit] = useState(String(channel?.rateLimitPerSecond ?? 100));
const [desiredConnections, setDesiredConnections] = useState(String(channel?.desiredConnections ?? 1));
const [windowSize, setWindowSize] = useState(String(channel?.windowSize ?? 16));
const [heartbeatIntervalSeconds, setHeartbeatIntervalSeconds] = useState(String(channel?.heartbeatIntervalSeconds ?? 30));
const [heartbeatMissThreshold, setHeartbeatMissThreshold] = useState(String(channel?.heartbeatMissThreshold ?? 3));
function submit() {
if (!isValidMoneyInput(unitPrice)) {
@@ -255,12 +260,15 @@ function ChannelFormModal({
failureCount: channel?.failureCount ?? 0,
gatewayHost,
gatewayPort,
businessCode: businessCode.trim() || 'SMS',
corpCode,
account,
accessNo,
cmppVersion,
desiredConnections: Number(desiredConnections) || 1,
windowSize: Number(windowSize) || 16,
heartbeatIntervalSeconds: Number(heartbeatIntervalSeconds) || 30,
heartbeatMissThreshold: Number(heartbeatMissThreshold) || 3,
extensionDigits: Number(extensionDigits),
rateLimitPerSecond: Number(flowLimit),
passwordCipher: password || undefined,
@@ -302,11 +310,12 @@ function ChannelFormModal({
<section>
<h3></h3>
<div className="sms-channel-form-grid">
<Select label="* 协议选择" onChange={(event) => setProtocol(event.target.value)} options={protocolOptions} value={protocol} />
<Input disabled label="* 协议选择" value="CMPP" />
<div className="sms-channel-inline-field">
<Input label="* 网关地址" onChange={(event) => setGatewayHost(event.target.value)} placeholder="请输入网关地址" value={gatewayHost} />
<Input label="端口" onChange={(event) => setGatewayPort(event.target.value)} value={gatewayPort} />
</div>
<Input hint="对应 CMPP Service_Id,最多 10 个 ASCII 字符。" label="* 业务代码" maxLength={10} onChange={(event) => setBusinessCode(event.target.value.toUpperCase())} value={businessCode} />
<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} />
@@ -326,6 +335,8 @@ function ChannelFormModal({
<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} />
<Input hint="平台主动向供应商发送 ACTIVE_TEST 的间隔" label="* 心跳间隔" min="1" onChange={(event) => setHeartbeatIntervalSeconds(event.target.value)} suffix="秒" type="number" value={heartbeatIntervalSeconds} />
<Input hint="连续未收到心跳响应达到该次数后重连" label="* 心跳失败阈值" min="1" onChange={(event) => setHeartbeatMissThreshold(event.target.value)} suffix="次" type="number" value={heartbeatMissThreshold} />
</div>
</section>
</div>
@@ -721,6 +732,10 @@ export function AdminChannelsPage() {
<span></span>
<strong>{formatDateTime(connection.lastHeartbeatAt)}</strong>
</div>
<div>
<span></span>
<strong>{connection.reconnectCount} / {formatDateTime(connection.nextReconnectAt)}</strong>
</div>
{connection.lastError ? <p>{connection.lastError}</p> : null}
</article>
))}