fix: 修复上行归属并实现签名质量日报优化

This commit is contained in:
hectorzhao
2026-09-17 11:12:58 +08:00
parent 4eb7b16d12
commit 572290308c
40 changed files with 2854 additions and 778 deletions
+114 -63
View File
@@ -1,11 +1,13 @@
import { useDeferredValue, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { BarChart3, Eye, Search, X } from 'lucide-react';
import {
adminApi,
type SignatureChannelCarrierQualityStat,
type SignatureChannelQualityItem,
type SignatureActivityResponse,
type SignatureAnalyticsMetadata,
type SignatureChannelQualityResponse,
type SignatureRetirementHeatmapItem,
type SignatureActivityItem,
type SignatureRetirementHeatmapDimension,
type UnreportedSignatureItem,
type PagedResult,
@@ -59,9 +61,19 @@ function AnalyticsPanel({ kind }: { kind: string }) {
const [pageSize, setPageSize] = useState(25);
const [appliedDate, setAppliedDate] = useState(() => shanghaiDateKey());
const requestId = useRef(0);
const abort = useRef<AbortController | null>(null);
const [activity, setActivity] = useState<SignatureActivityResponse | null>(null);
const [activityFilters, setActivityFilters] = useState({
tenantName: '',
applicationName: '',
signatureName: '',
channelName: '',
});
const [appliedActivityFilters, setAppliedActivityFilters] = useState(activityFilters);
const [metadata, setMetadata] = useState<SignatureAnalyticsMetadata | null>(null);
const [statisticsDate, setStatisticsDate] = useState(() => shanghaiDateKey());
const [signatureQuality, setSignatureQuality] = useState<SignatureChannelQualityResponse | null>(null);
const [retirementHeatmap, setRetirementHeatmap] = useState<SignatureRetirementHeatmapItem[]>([]);
const [retirementHeatmap, setRetirementHeatmap] = useState<SignatureActivityItem[]>([]);
const [retirementDimensions, setRetirementDimensions] = useState<SignatureRetirementHeatmapDimension[]>([]);
const [unreportedSignatures, setUnreportedSignatures] = useState<
(PagedResult<UnreportedSignatureItem> & { date: string }) | null
@@ -80,30 +92,47 @@ function AnalyticsPanel({ kind }: { kind: string }) {
unreported = appliedUnreportedKeyword,
date = statisticsDate,
size = pageSize,
filters = appliedActivityFilters,
) {
abort.current?.abort();
const controller = new AbortController();
abort.current = controller;
const id = ++requestId.current;
setLoading(true);
setError('');
try {
if (kind === 'quality') {
const data = await adminApi.getSignatureQuality({ date, keyword: keyword || undefined, page, pageSize: size });
const data = await adminApi.getSignatureQuality(
{ date, keyword: keyword || undefined, page, pageSize: size },
controller.signal,
);
if (id !== requestId.current) return;
setSignatureQuality(data);
setMetadata(data);
setAppliedKeyword(keyword);
setSelectedSignature(null);
} else if (kind === 'unreported') {
const data = await adminApi.getUnreportedSignatures({
date,
keyword: unreported || undefined,
page,
pageSize: size,
});
const data = await adminApi.getUnreportedSignatures(
{
date,
keyword: unreported || undefined,
page,
pageSize: size,
},
controller.signal,
);
if (id !== requestId.current) return;
setUnreportedSignatures(data);
setMetadata(data);
setAppliedUnreportedKeyword(unreported);
} else {
const data = await adminApi.getSignatureRetirementHeatmap(date);
const data = await adminApi.getSignatureActivity(
{ date, dimensionType: kind as 'enterprise' | 'channel', page, pageSize: size, ...filters },
controller.signal,
);
if (id !== requestId.current) return;
setActivity(data);
setAppliedActivityFilters(filters);
setRetirementHeatmap(data.items);
setRetirementDimensions(data.dimensions);
}
@@ -115,9 +144,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
}
}
const initialLoad = useRef(loadData);
useEffect(() => {
void loadData(1, '');
void initialLoad.current(1, '');
return () => {
abort.current?.abort();
requestId.current += 1;
};
}, []);
@@ -221,7 +252,7 @@ function AnalyticsPanel({ kind }: { kind: string }) {
];
function queryStatistics() {
void loadData(1, signatureKeyword.trim());
void loadData(1, signatureKeyword.trim(), unreportedKeyword.trim(), statisticsDate, pageSize, activityFilters);
}
function changeSignaturePage(page: number) {
@@ -233,8 +264,7 @@ function AnalyticsPanel({ kind }: { kind: string }) {
function changePageSize(size: number) {
setPageSize(size);
if (kind === 'quality' || kind === 'unreported')
void loadData(1, appliedKeyword, appliedUnreportedKeyword, appliedDate, size);
void loadData(1, appliedKeyword, appliedUnreportedKeyword, appliedDate, size);
}
return (
@@ -256,7 +286,28 @@ function AnalyticsPanel({ kind }: { kind: string }) {
</Button>
</div>
</div>
{error ? <p className="form-error">{error}</p> : null}
{error ? (
<p className="form-error">
{error}
{signatureQuality || activity || unreportedSignatures
? `;仍显示上次成功查询的数据,日期为 ${appliedDate}`
: ';尚无成功查询的数据。'}
</p>
) : null}
{metadata ? (
<p className="muted" role="status">
{metadata.dataSource === 'live' ? '当天实时查询' : metadata.frozen ? '已冻结日报' : '近三日可刷新日报'} ·{' '}
{metadata.reportState === 'missing'
? '报表尚未生成'
: metadata.reportState === 'failed'
? '生成失败,保留上次完整日报'
: metadata.reportState === 'refreshing'
? '正在刷新,显示上次完整日报'
: '查询完成'}
{metadata.provenance === 'backfill-current-source' ? ' · 事后补建' : ''}
{metadata.sourceAsOf ? ` · 数据截止 ${new Date(metadata.sourceAsOf).toLocaleString('zh-CN')}` : ''}
</p>
) : null}
{kind === 'quality' ? (
<div className="surface signature-quality-card">
@@ -323,6 +374,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
onPageSizeChange={changePageSize}
date={appliedDate}
dimensionType="enterprise"
activity={activity}
filters={activityFilters}
onFilterChange={setActivityFilters}
onSearch={queryStatistics}
onPageChange={(page) => void loadData(page, appliedKeyword, appliedUnreportedKeyword, appliedDate)}
dimensions={retirementDimensions}
items={retirementHeatmap}
title="企业签名活跃度热力图"
@@ -334,6 +390,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
onPageSizeChange={changePageSize}
date={appliedDate}
dimensionType="channel"
activity={activity}
filters={activityFilters}
onFilterChange={setActivityFilters}
onSearch={queryStatistics}
onPageChange={(page) => void loadData(page, appliedKeyword, appliedUnreportedKeyword, appliedDate)}
dimensions={retirementDimensions}
items={retirementHeatmap}
title="通道签名活跃度热力图"
@@ -349,7 +410,7 @@ function AnalyticsPanel({ kind }: { kind: string }) {
loading={loading}
onKeywordChange={setUnreportedKeyword}
onPageChange={(page) => loadUnreportedSignatures(page)}
onSearch={() => loadUnreportedSignatures(1, unreportedKeyword.trim())}
onSearch={queryStatistics}
/>
) : null}
@@ -365,6 +426,11 @@ function AnalyticsPanel({ kind }: { kind: string }) {
}
function RetirementHeatmap({
activity,
filters,
onFilterChange,
onSearch,
onPageChange,
pageSize,
onPageSizeChange,
date,
@@ -378,12 +444,14 @@ function RetirementHeatmap({
date: string;
dimensionType: 'enterprise' | 'channel';
dimensions: SignatureRetirementHeatmapDimension[];
items: SignatureRetirementHeatmapItem[];
items: SignatureActivityItem[];
title: string;
activity: SignatureActivityResponse | null;
filters: { tenantName: string; applicationName: string; signatureName: string; channelName: string };
onFilterChange: (value: typeof filters) => void;
onSearch: () => void;
onPageChange: (page: number) => void;
}) {
const [pageState, setPageState] = useState({ key: '', page: 1 });
const [filters, setFilters] = useState({ tenantName: '', applicationName: '', signatureName: '', channelName: '' });
const deferredFilters = useDeferredValue(filters);
const visible = items.filter((item) => item.dimensionType === dimensionType);
const dates = previousDateKeys(date, 30);
const cellMap = new Map(
@@ -392,40 +460,17 @@ function RetirementHeatmap({
item,
]),
);
const rows = dimensions
.filter((item) => item.dimensionType === dimensionType)
.filter((item) =>
Object.entries(deferredFilters).every(
([key, value]) =>
!value.trim() ||
(item[key as keyof typeof deferredFilters] ?? '')
.toLocaleLowerCase('zh-CN')
.includes(value.trim().toLocaleLowerCase('zh-CN')),
),
)
.map((item) => ({
key: `${item.signatureId}:${item.channelId ?? ''}:${item.carrier}`,
signatureName: item.signatureName,
channelName: item.channelName,
tenantName: item.tenantName,
applicationName: item.applicationName,
carrier: item.carrier,
approvedAt: item.approvedAt.slice(0, 10),
total: dates.reduce(
(sum, dateKey) =>
sum +
(cellMap.get(`${item.signatureId}:${item.channelId ?? ''}:${item.carrier}:${dateKey}`)
?.acceptedBusinessCount ?? 0),
0,
),
}))
.sort((left, right) => right.total - left.total || left.signatureName.localeCompare(right.signatureName, 'zh-CN'));
const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
const paginationKey = JSON.stringify([date, deferredFilters, dimensionType, dimensions.length, pageSize]);
const page = pageState.key === paginationKey ? pageState.page : 1;
const setPage = (value: number) => setPageState({ key: paginationKey, page: value });
const currentPage = Math.min(page, totalPages);
const pagedRows = rows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const rows = dimensions.map((item) => ({
...item,
key: `${item.signatureId}:${item.channelId ?? ''}:${item.carrier}`,
approvedAt: item.approvedAt?.slice(0, 10) ?? '',
total: (item as SignatureRetirementHeatmapDimension & { total: number }).total ?? 0,
}));
const totalPages = Math.max(1, Math.ceil((activity?.total ?? 0) / pageSize));
const currentPage = activity?.page ?? 1;
const pagedRows = rows;
const setPage = onPageChange;
const coverage = new Map(activity?.coverage.map((day) => [day.date, day]));
return (
<div className="surface signature-retirement-heatmap">
@@ -448,12 +493,18 @@ function RetirementHeatmap({
label={label}
placeholder={`搜索${label}`}
value={filters[key]}
onChange={(event) => setFilters((current) => ({ ...current, [key]: event.target.value }))}
onChange={(event) => onFilterChange({ ...filters, [key]: event.target.value })}
/>
))}
<Tag tone="info">T-1 T-30</Tag>
<Button onClick={onSearch} icon={<Search size={16} />}>
</Button>
<Tag tone="info">30</Tag>
</div>
</div>
{activity && !activity.complete ? (
<p className="form-error">30</p>
) : null}
{rows.length ? (
<>
<div className="signature-retirement-heatmap__scroll">
@@ -482,7 +533,7 @@ function RetirementHeatmap({
<td className="signature-retirement-heatmap__total">{row.total.toLocaleString('zh-CN')}</td>
{dates.map((dateKey) => {
const item = cellMap.get(`${row.key}:${dateKey}`);
const beforeApproval = dateKey < row.approvedAt;
const beforeApproval = Boolean(coverage.get(dateKey)?.generationId && !item);
const successRate = item?.acceptedBusinessCount
? (item.deliveredBusinessCount / item.acceptedBusinessCount) * 100
: 0;
@@ -494,10 +545,10 @@ function RetirementHeatmap({
? 'is-zero'
: `is-rate-${successRateTone(successRate)}`;
const titleText = beforeApproval
? '报备前,不适用'
? '当日报备维度不适用'
: item
? `提交条数:${item.submittedAttempts}\n上游接受条数:${item.acceptedBusinessCount}\n发送成功条数:${item.deliveredBusinessCount}\n发送成功率:${successRate.toFixed(1)}%\n检测状态${item.status === 'observing' ? '观察中(不预警)' : item.status === 'alert' ? '预警' : '正常'}\n预警阈值:${item.threshold}`
: '当日无检测快照';
? `提交条数:${item.submittedAttempts}\n上游接受条数:${item.acceptedBusinessCount}\n发送成功条数:${item.deliveredBusinessCount}\n发送成功率:${successRate.toFixed(1)}%\n数据来源${coverage.get(dateKey)?.frozen ? '冻结日报' : '可刷新日报'}`
: '当日报表尚未生成';
return (
<td className={className} key={dateKey} title={titleText}>
{beforeApproval ? 'N/A' : item ? item.acceptedBusinessCount : '—'}
@@ -512,7 +563,7 @@ function RetirementHeatmap({
</>
) : (
<p className="empty-state">
{Object.values(deferredFilters).some((value) => value.trim())
{Object.values(filters).some((value) => value.trim())
? '没有匹配企业、企业应用或签名的热力图维度。'
: '暂无已确认到运营商的报备事实,尚未形成检测热力图。'}
</p>
@@ -526,7 +577,7 @@ function RetirementHeatmap({
onPrevious={() => setPage(currentPage - 1)}
page={currentPage}
previousDisabled={currentPage <= 1}
total={rows.length}
total={activity?.total ?? 0}
totalPages={totalPages}
/>
</div>