修复:补齐风控解除封禁操作

This commit is contained in:
linkeyu
2026-07-25 15:59:30 +08:00
parent fb5efcf6d6
commit a3577401b4
3 changed files with 182 additions and 24 deletions
@@ -87,6 +87,34 @@
margin-bottom: 16px;
}
.statusToolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 2px;
padding: 12px 16px;
border: 1px solid #ecece8;
border-radius: 12px;
background: #fff;
}
.statusToolbar > div {
display: flex;
flex-direction: column;
gap: 2px;
}
.statusToolbar strong {
color: #222;
font-size: 14px;
}
.statusToolbar span {
color: #888;
font-size: 12px;
}
.summaryCard {
min-height: 148px;
padding: 26px 24px 20px;
@@ -320,4 +348,9 @@
.updatedMeta {
display: none;
}
.statusToolbar {
align-items: flex-start;
flex-direction: column;
}
}
+148 -24
View File
@@ -7,6 +7,7 @@ import {
Empty,
InputNumber,
Modal,
Segmented,
Skeleton,
Table,
Tag,
@@ -64,6 +65,7 @@ const KIND_META: Record<
const integer = (value: number) => new Intl.NumberFormat('zh-CN').format(value);
const LIST_PAGE_SIZE = 20;
const RISK_KINDS: RiskKind[] = ['sms', 'oneclick', 'compare'];
type IncidentStatus = 'open' | 'blocked';
const RULE_LIMITS: Record<RiskKind, { min: number; max: number; help: string }> = {
sms: {
min: 1,
@@ -277,6 +279,7 @@ export default function RiskMonitorPage() {
oneclick: 1,
compare: 1,
});
const [incidentStatus, setIncidentStatus] = useState<IncidentStatus>('open');
const listPageRef = useRef(listPage);
const [loading, setLoading] = useState(true);
const [expanded, setExpanded] = useState<Set<number>>(new Set());
@@ -300,13 +303,25 @@ export default function RiskMonitorPage() {
const [summaryRes, smsRes, oneclickRes, compareRes] = await Promise.all([
api.get<RiskMonitorSummary>('/admin/api/risk-monitor/summary'),
api.get<RiskIncidentPage>('/admin/api/risk-monitor/incidents/sms', {
params: { cursor: (pages.sms - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE },
params: {
status: incidentStatus,
cursor: (pages.sms - 1) * LIST_PAGE_SIZE,
limit: LIST_PAGE_SIZE,
},
}),
api.get<RiskIncidentPage>('/admin/api/risk-monitor/incidents/oneclick', {
params: { cursor: (pages.oneclick - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE },
params: {
status: incidentStatus,
cursor: (pages.oneclick - 1) * LIST_PAGE_SIZE,
limit: LIST_PAGE_SIZE,
},
}),
api.get<RiskIncidentPage>('/admin/api/risk-monitor/incidents/compare', {
params: { cursor: (pages.compare - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE },
params: {
status: incidentStatus,
cursor: (pages.compare - 1) * LIST_PAGE_SIZE,
limit: LIST_PAGE_SIZE,
},
}),
]);
setSummary(summaryRes.data);
@@ -320,7 +335,7 @@ export default function RiskMonitorPage() {
} finally {
setLoading(false);
}
}, [message]);
}, [incidentStatus, message]);
const changeListPage = useCallback(
async (kind: RiskKind, page: number) => {
@@ -331,7 +346,11 @@ export default function RiskMonitorPage() {
const { data } = await api.get<RiskIncidentPage>(
`/admin/api/risk-monitor/incidents/${kind}`,
{
params: { cursor: (page - 1) * LIST_PAGE_SIZE, limit: LIST_PAGE_SIZE },
params: {
status: incidentStatus,
cursor: (page - 1) * LIST_PAGE_SIZE,
limit: LIST_PAGE_SIZE,
},
},
);
setLists((current) => ({ ...current, [kind]: data }));
@@ -339,9 +358,21 @@ export default function RiskMonitorPage() {
void message.error(errMsg(error, '风险列表加载失败'));
}
},
[message],
[incidentStatus, message],
);
const changeIncidentStatus = useCallback((status: IncidentStatus) => {
const firstPage = { sms: 1, oneclick: 1, compare: 1 };
listPageRef.current = firstPage;
setListPage(firstPage);
setLists({});
setExpanded(new Set());
setDetails({});
setDetailLoading(new Set());
setDetailPage({});
setIncidentStatus(status);
}, []);
useEffect(() => {
void loadAll();
const timer = window.setInterval(() => void loadAll(true), 60 * 60 * 1000);
@@ -516,6 +547,56 @@ export default function RiskMonitorPage() {
[lists, loadAll, message, modal],
);
const revokeRestriction = useCallback(
(row: RiskIncidentItem) => {
if (!row.restriction_id) {
void message.error('未找到有效的封禁记录,请刷新后重试');
return;
}
modal.confirm({
title: `确认解除该${row.subject_type === 'device' ? '设备' : '账户'}的封禁?`,
content:
row.subject_type === 'device'
? '解除后,该设备可以重新发送短信验证码和使用一键登录。'
: '解除后,该账户可以重新发起比价、领取任务奖励和提现。',
okText: '解除封禁',
cancelText: '取消',
async onOk() {
setActing(row.incident_id);
try {
await api.post(
`/admin/api/risk-monitor/restrictions/${row.restriction_id}/revoke`,
);
void message.success('解除封禁成功');
setExpanded((current) => {
const next = new Set(current);
next.delete(row.incident_id);
return next;
});
if (
(lists[row.kind]?.items.length ?? 0) === 1
&& listPageRef.current[row.kind] > 1
) {
const nextPages = {
...listPageRef.current,
[row.kind]: listPageRef.current[row.kind] - 1,
};
listPageRef.current = nextPages;
setListPage(nextPages);
}
await loadAll(true);
} catch (error) {
void message.error(errMsg(error, '解除封禁失败'));
throw error;
} finally {
setActing(null);
}
},
});
},
[lists, loadAll, message, modal],
);
const columns = useCallback(
(kind: RiskKind): ColumnsType<RiskIncidentItem> => {
const actionColumn = {
@@ -537,21 +618,35 @@ export default function RiskMonitorPage() {
>
{open ? '收起' : '展开'}
</Button>
<Button
size="small"
danger
loading={acting === row.incident_id}
onClick={() => runAction(row, 'block')}
>
</Button>
<Button
size="small"
loading={acting === row.incident_id}
onClick={() => runAction(row, 'ignore')}
>
</Button>
{row.status === 'open' && (
<>
<Button
size="small"
danger
loading={acting === row.incident_id}
onClick={() => runAction(row, 'block')}
>
</Button>
<Button
size="small"
loading={acting === row.incident_id}
onClick={() => runAction(row, 'ignore')}
>
</Button>
</>
)}
{row.status === 'blocked' && (
<Button
size="small"
loading={acting === row.incident_id}
disabled={!row.restriction_id}
onClick={() => revokeRestriction(row)}
>
</Button>
)}
</span>
);
},
@@ -605,7 +700,7 @@ export default function RiskMonitorPage() {
actionColumn,
];
},
[acting, copy, detailLoading, expanded, runAction, toggle],
[acting, copy, detailLoading, expanded, revokeRestriction, runAction, toggle],
);
const cards = useMemo(() => {
@@ -688,6 +783,25 @@ export default function RiskMonitorPage() {
})}
</section>
<div className={styles.statusToolbar}>
<div>
<strong></strong>
<span>
{incidentStatus === 'open'
? '查看待处理报警,可执行封禁或忽略'
: '查看当前已封禁主体,可恢复其业务权限'}
</span>
</div>
<Segmented
value={incidentStatus}
options={[
{ label: '待处理', value: 'open' },
{ label: '已封禁', value: 'blocked' },
]}
onChange={(value) => changeIncidentStatus(value as IncidentStatus)}
/>
</div>
{RISK_KINDS.map((kind) => {
const data = lists[kind]?.items ?? [];
return (
@@ -700,7 +814,17 @@ export default function RiskMonitorPage() {
dataSource={data}
tableLayout="fixed"
scroll={{ x: kind === 'compare' ? 900 : 890 }}
locale={{ emptyText: <Empty description="当前没有待处理风险" /> }}
locale={{
emptyText: (
<Empty
description={
incidentStatus === 'open'
? '当前没有待处理风险'
: '当前没有已封禁主体'
}
/>
),
}}
pagination={{
current: listPage[kind],
pageSize: LIST_PAGE_SIZE,
@@ -728,7 +852,7 @@ export default function RiskMonitorPage() {
<footer className={styles.footerNote}>
<strong></strong>
线
线
</footer>
</>
)}
+1
View File
@@ -268,6 +268,7 @@ export interface RiskIncidentItem {
event_count: number;
status: 'open' | 'ignored' | 'blocked' | 'resolved';
restricted: boolean;
restriction_id: number | null;
}
export interface RiskIncidentPage {