feat: improve operations diagnostics and channel management

This commit is contained in:
hectorzhao
2026-08-09 14:27:19 +08:00
parent 44352aeb2f
commit 4724b9db6a
65 changed files with 1211 additions and 293 deletions
+70 -53
View File
@@ -4,14 +4,20 @@ import {
adminApi,
type SendQualityResponse,
type SignatureChannelCarrierQualityStat,
type SignatureChannelCarrierDrainageQualityStat,
type SignatureChannelQualityItem,
type SignatureChannelQualityResponse,
} from '@/api/adminApi';
import { Breadcrumb, Button, Chart, Input, Pagination, Table, Tag, type TableColumn } from '@/components/ui';
import { createBarOption, createPieOption } from '@/theme/chartOptions';
import { successRateClassName } from '@/utils/successRate';
const carrierOrder = ['mobile', 'unicom', 'telecom', 'unknown'];
const majorCarrierOrder = ['mobile', 'unicom', 'telecom'] as const;
const drainageStates = [
{ value: 'with', label: '含引流' },
{ value: 'without', label: '不含引流' },
{ value: 'unknown', label: '未检测' },
] as const;
const carrierLabels: Record<string, string> = {
mobile: '移动',
unicom: '联通',
@@ -308,7 +314,8 @@ function SignatureQualityDrawer({
return (leftRank < 0 ? carrierOrder.length : leftRank)
- (rightRank < 0 ? carrierOrder.length : rightRank);
});
const channels = [...new Map(item.breakdowns.map((entry) => [entry.channelId, entry.channelName])).entries()]
const channels = [...new Map([...item.breakdowns, ...item.drainageBreakdowns]
.map((entry) => [entry.channelId, entry.channelName])).entries()]
.map(([channelId, channelName]) => ({ channelId, channelName }));
const visibleCarriers = carrierOrder.filter((carrier) => item.breakdowns.some((entry) => normalizeCarrier(entry.carrier) === carrier));
@@ -330,7 +337,7 @@ function SignatureQualityDrawer({
<div className="signature-quality-overview">
<QualityMetric label="业务短信" value={item.total.toLocaleString('zh-CN')} />
<QualityMetric label="通道提交" value={item.channelSubmitTotal.toLocaleString('zh-CN')} />
<QualityMetric label="最终成功率" tone={rateTone(item.successRate)} value={`${item.successRate.toFixed(1)}%`} />
<QualityMetric label="最终成功率" value={`${item.successRate.toFixed(1)}%`} valueClassName={successRateClassName(item.successRate)} />
<QualityMetric label="平均到达时间" value={formatDuration(item.averageArrivalMs)} />
</div>
@@ -349,7 +356,7 @@ function SignatureQualityDrawer({
<strong>{carrier.businessMessageCount.toLocaleString('zh-CN')} </strong>
</div>
<dl>
<div><dt></dt><dd>{carrier.finalSuccessRate.toFixed(1)}%</dd></div>
<div><dt></dt><dd className={successRateClassName(carrier.finalSuccessRate)}>{carrier.finalSuccessRate.toFixed(1)}%</dd></div>
<div><dt></dt><dd>{formatDuration(carrier.averageArrivalMs)}</dd></div>
<div><dt></dt><dd>{carrier.channelCount} </dd></div>
</dl>
@@ -362,39 +369,59 @@ function SignatureQualityDrawer({
<div className="signature-quality-section__heading">
<div>
<h3> × </h3>
<p>{matrixMode === 'overall' ? '整体口径展示该组合全部真实提交。' : '引流切分口径分别展示含引流、不含引流和历史未检测数据。'}</p>
<p>{matrixMode === 'overall'
? '整体口径展示该组合全部真实提交;“—”表示所选日期没有真实提交。'
: '固定按含引流、不含引流、未检测三行及移动、联通、电信三列展示;没有真实提交的组合显示 0。'}</p>
</div>
<div className="page-actions"><Button onClick={() => setMatrixMode('overall')} size="sm" variant={matrixMode === 'overall' ? 'primary' : 'ghost'}></Button><Button onClick={() => setMatrixMode('drainage')} size="sm" variant={matrixMode === 'drainage' ? 'primary' : 'ghost'}></Button></div>
</div>
<div className="signature-quality-matrix">
<table>
<thead>
<tr>
<th></th>
{visibleCarriers.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
</tr>
{matrixMode === 'overall' ? (
<tr>
<th></th>
{visibleCarriers.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
</tr>
) : (
<tr>
<th></th>
<th></th>
{majorCarrierOrder.map((carrier) => <th key={carrier}>{carrierLabel(carrier)}</th>)}
</tr>
)}
</thead>
<tbody>
{channels.map((channel) => (
<tr key={channel.channelId}>
<th>{channel.channelName}</th>
{visibleCarriers.map((carrier) => {
const metric = item.breakdowns.find((entry) => (
entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier
));
const drainageMetrics = item.drainageBreakdowns.filter((entry) => (
entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier
));
return (
<td key={carrier}>
{matrixMode === 'overall'
? metric ? <MatrixMetric metric={metric} /> : <span className="signature-quality-matrix__empty"></span>
: drainageMetrics.length ? <DrainageMatrixMetrics metrics={drainageMetrics} /> : <span className="signature-quality-matrix__empty"></span>}
</td>
);
})}
</tr>
))}
{matrixMode === 'overall'
? channels.map((channel) => (
<tr key={channel.channelId}>
<th>{channel.channelName}</th>
{visibleCarriers.map((carrier) => {
const metric = item.breakdowns.find((entry) => (
entry.channelId === channel.channelId && normalizeCarrier(entry.carrier) === carrier
));
return (
<td key={carrier}>
{metric ? <MatrixMetric metric={metric} /> : <span className="signature-quality-matrix__empty"></span>}
</td>
);
})}
</tr>
))
: channels.flatMap((channel) => drainageStates.map((state, stateIndex) => (
<tr key={`${channel.channelId}-${state.value}`}>
{stateIndex === 0 ? <th rowSpan={drainageStates.length}>{channel.channelName}</th> : null}
<th className="signature-quality-matrix__drainage-label">{state.label}</th>
{majorCarrierOrder.map((carrier) => {
const metric = item.drainageBreakdowns.find((entry) => (
entry.channelId === channel.channelId
&& normalizeCarrier(entry.carrier) === carrier
&& entry.drainageState === state.value
));
return <td key={carrier}><MatrixMetric metric={metric} zeroWhenEmpty /></td>;
})}
</tr>
)))}
</tbody>
</table>
</div>
@@ -409,41 +436,37 @@ function SignatureQualityDrawer({
);
}
function QualityMetric({ label, value, tone = 'default' }: { label: string; value: string; tone?: string }) {
function QualityMetric({ label, value, valueClassName }: { label: string; value: string; valueClassName?: string }) {
return (
<div className={`signature-quality-metric signature-quality-metric--${tone}`}>
<div className="signature-quality-metric">
<span>{label}</span>
<strong>{value}</strong>
<strong className={valueClassName}>{value}</strong>
</div>
);
}
function MatrixMetric({ metric }: { metric: SignatureChannelCarrierQualityStat }) {
function MatrixMetric({ metric, zeroWhenEmpty = false }: { metric?: SignatureChannelCarrierQualityStat; zeroWhenEmpty?: boolean }) {
const total = metric?.total ?? 0;
const successRate = metric?.successRate ?? 0;
if (zeroWhenEmpty && total === 0) return <span className="signature-quality-matrix__zero">0</span>;
return (
<div className="signature-quality-matrix__metric">
<strong>{metric.total.toLocaleString('zh-CN')} </strong>
<span className={`signature-quality-matrix__rate signature-quality-matrix__rate--${rateTone(metric.successRate)}`}>
{metric.successRate.toFixed(1)}%
<strong>{total.toLocaleString('zh-CN')} </strong>
<span className={`signature-quality-matrix__rate ${successRateClassName(successRate)}`}>
{successRate.toFixed(1)}%
</span>
<small>{formatDuration(metric.averageArrivalMs)}</small>
{metric.submitFailureCount > 0 ? <em> {metric.submitFailureCount}</em> : null}
<small>{formatDuration(metric?.averageArrivalMs)}</small>
{(metric?.submitFailureCount ?? 0) > 0 ? <em> {metric?.submitFailureCount}</em> : null}
</div>
);
}
function DrainageMatrixMetrics({ metrics }: { metrics: SignatureChannelCarrierDrainageQualityStat[] }) {
const labels = { with: '含引流', without: '不含引流', unknown: '未检测' };
return <div className="signature-quality-matrix__drainage">{(['with', 'without', 'unknown'] as const).map((state) => {
const metric = metrics.find((item) => item.drainageState === state);
return metric ? <div key={state}><b>{labels[state]}</b><MatrixMetric metric={metric} /></div> : null;
})}</div>;
}
function QualityRate({ value }: { value: number }) {
return (
<div className="signature-quality-rate">
<div><span style={{ width: `${Math.min(100, Math.max(0, value))}%` }} /></div>
<strong className={`signature-quality-rate--${rateTone(value)}`}>{value.toFixed(1)}%</strong>
<strong className={successRateClassName(value)}>{value.toFixed(1)}%</strong>
</div>
);
}
@@ -468,12 +491,6 @@ function carrierTagTone(value: string): 'info' | 'accent' | 'warning' | 'neutral
return 'neutral';
}
function rateTone(value: number) {
if (value >= 98) return 'success';
if (value >= 95) return 'warning';
return 'danger';
}
function formatDuration(value?: number | null) {
if (value == null) return '—';
if (value < 1000) return `${Math.round(value)} 毫秒`;