修复:完善限制策略页面稳定性
统一白名单时间口径并接入带乱序保护的分页列表;优化风控页路由跳转、无手机号禁用提示及后端分组计数复用。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
@@ -26,6 +26,8 @@ import { EditOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import dayjs, { type Dayjs } from 'dayjs';
|
||||
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { formatUtcTime, utcDayjs } from '@/lib/format';
|
||||
import { usePagedList } from '@/lib/usePagedList';
|
||||
import type {
|
||||
LimitOverride,
|
||||
LimitRule,
|
||||
@@ -71,8 +73,7 @@ interface WhitelistFilters {
|
||||
|
||||
const asIso = (value?: number | null) =>
|
||||
value ? dayjs(value).toISOString() : null;
|
||||
const showTime = (value: string | null) =>
|
||||
value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-';
|
||||
const showTime = (value: string | null) => formatUtcTime(value);
|
||||
const normalizeSubjectValue = (
|
||||
subjectType: LimitSubjectType,
|
||||
value: string,
|
||||
@@ -148,11 +149,6 @@ function RuleSelector({
|
||||
export default function LimitWhitelistPage() {
|
||||
const { message, modal } = App.useApp();
|
||||
const [rules, setRules] = useState<LimitRule[]>([]);
|
||||
const [rows, setRows] = useState<LimitSubject[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [subjectType, setSubjectType] = useState<LimitSubjectType | undefined>();
|
||||
const [ruleFilter, setRuleFilter] = useState<string | undefined>();
|
||||
@@ -180,37 +176,45 @@ export default function LimitWhitelistPage() {
|
||||
createPreset?.subject_type ||
|
||||
'phone';
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [ruleResponse, overrideResponse] = await Promise.all([
|
||||
api.get<LimitRule[]>('/admin/api/limit-whitelist/rules'),
|
||||
api.get<LimitSubjectList>('/admin/api/limit-whitelist/subjects', {
|
||||
params: {
|
||||
keyword: appliedFilters.keyword.trim() || undefined,
|
||||
subject_type: appliedFilters.subjectType,
|
||||
rule_code: appliedFilters.ruleCode,
|
||||
offset: (page - 1) * pageSize,
|
||||
limit: pageSize,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
setRules(ruleResponse.data);
|
||||
setRows(overrideResponse.data.items);
|
||||
setTotal(overrideResponse.data.total);
|
||||
} catch (error) {
|
||||
message.error(errMsg(error, '白名单数据加载失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [appliedFilters, message, page, pageSize]);
|
||||
const subjectFilters = useMemo(
|
||||
() => ({
|
||||
keyword: appliedFilters.keyword.trim() || undefined,
|
||||
subject_type: appliedFilters.subjectType,
|
||||
rule_code: appliedFilters.ruleCode,
|
||||
}),
|
||||
[appliedFilters],
|
||||
);
|
||||
const {
|
||||
items: rows,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
loading,
|
||||
onChange: onPageChange,
|
||||
reload: reloadSubjects,
|
||||
} = usePagedList<LimitSubject>(
|
||||
'/admin/api/limit-whitelist/subjects',
|
||||
subjectFilters,
|
||||
10,
|
||||
'offset',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
let active = true;
|
||||
void api
|
||||
.get<LimitRule[]>('/admin/api/limit-whitelist/rules')
|
||||
.then((response) => {
|
||||
if (active) setRules(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (active) message.error(errMsg(error, '限制规则加载失败'));
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [message]);
|
||||
|
||||
const applyFilters = () => {
|
||||
setPage(1);
|
||||
setAppliedFilters({
|
||||
keyword,
|
||||
subjectType,
|
||||
@@ -290,8 +294,8 @@ export default function LimitWhitelistPage() {
|
||||
enabled: editing.items.every((item) => item.enabled),
|
||||
valid_range: expiresAt
|
||||
? [
|
||||
startsAt[0] ? dayjs(startsAt[0]).valueOf() : Date.now(),
|
||||
dayjs(expiresAt).valueOf(),
|
||||
startsAt[0] ? utcDayjs(startsAt[0]).valueOf() : Date.now(),
|
||||
utcDayjs(expiresAt).valueOf(),
|
||||
]
|
||||
: null,
|
||||
reason: reasons.size === 1 ? Array.from(reasons)[0] : '',
|
||||
@@ -417,7 +421,7 @@ export default function LimitWhitelistPage() {
|
||||
if (new URLSearchParams(window.location.search).get('create') === '1') {
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
await load();
|
||||
await reloadSubjects();
|
||||
} catch (error) {
|
||||
message.error(errMsg(error));
|
||||
} finally {
|
||||
@@ -434,7 +438,10 @@ export default function LimitWhitelistPage() {
|
||||
});
|
||||
message.success('全局限制已更新,业务接口即时读取新值');
|
||||
setGlobalRule(null);
|
||||
await load();
|
||||
const response = await api.get<LimitRule[]>(
|
||||
'/admin/api/limit-whitelist/rules',
|
||||
);
|
||||
setRules(response.data);
|
||||
} catch (error) {
|
||||
message.error(errMsg(error));
|
||||
} finally {
|
||||
@@ -452,10 +459,10 @@ export default function LimitWhitelistPage() {
|
||||
enabled,
|
||||
});
|
||||
message.success(enabled ? '白名单已启用' : '白名单已停用');
|
||||
await load();
|
||||
await reloadSubjects();
|
||||
} catch (error) {
|
||||
message.error(errMsg(error, '白名单启用状态更新失败'));
|
||||
await load();
|
||||
await reloadSubjects();
|
||||
} finally {
|
||||
setTogglingSubject(null);
|
||||
}
|
||||
@@ -525,12 +532,7 @@ export default function LimitWhitelistPage() {
|
||||
title: '解除限制项',
|
||||
key: 'rule',
|
||||
render: (_, row) => {
|
||||
const groupCounts = Object.fromEntries(
|
||||
RULE_GROUPS.map((group) => [
|
||||
group,
|
||||
row.items.filter((item) => item.rule_group === group).length,
|
||||
]),
|
||||
);
|
||||
const groupCounts = row.group_counts;
|
||||
return (
|
||||
<Popover
|
||||
trigger={['hover', 'click']}
|
||||
@@ -863,12 +865,7 @@ export default function LimitWhitelistPage() {
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
showTotal: (count) => `共 ${count} 个主体`,
|
||||
onChange: (nextPage, nextPageSize) => {
|
||||
setPage(
|
||||
nextPageSize !== pageSize ? 1 : nextPage,
|
||||
);
|
||||
setPageSize(nextPageSize);
|
||||
},
|
||||
onChange: onPageChange,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { formatUtcTime, utcDayjs } from '@/lib/format';
|
||||
import type {
|
||||
@@ -272,6 +273,7 @@ function DetailTable({
|
||||
}
|
||||
|
||||
export default function RiskMonitorPage() {
|
||||
const router = useRouter();
|
||||
const { message, modal } = App.useApp();
|
||||
const [summary, setSummary] = useState<RiskMonitorSummary | null>(null);
|
||||
const [lists, setLists] = useState<Partial<Record<RiskKind, RiskIncidentPage>>>({});
|
||||
@@ -627,9 +629,9 @@ export default function RiskMonitorPage() {
|
||||
subject_value: subjectValue,
|
||||
rule_code: ruleCode[row.kind],
|
||||
});
|
||||
window.location.assign(`/limit-whitelist?${params.toString()}`);
|
||||
router.push(`/limit-whitelist?${params.toString()}`);
|
||||
},
|
||||
[message],
|
||||
[message, router],
|
||||
);
|
||||
|
||||
const columns = useCallback(
|
||||
@@ -642,9 +644,15 @@ export default function RiskMonitorPage() {
|
||||
width: 280,
|
||||
render: (_: unknown, row: RiskIncidentItem) => {
|
||||
const open = expanded.has(row.incident_id);
|
||||
const canAddToWhitelist =
|
||||
row.kind === 'compare' ||
|
||||
!row.subject_id.startsWith('legacy-ip:');
|
||||
const whitelistUnavailableReason =
|
||||
row.kind === 'compare'
|
||||
? row.phone
|
||||
? undefined
|
||||
: '该风险记录没有关联手机号,暂时无法加入白名单'
|
||||
: row.subject_id.startsWith('legacy-ip:')
|
||||
? '旧客户端未上报真实设备 ID,无法加入设备白名单'
|
||||
: undefined;
|
||||
const canAddToWhitelist = !whitelistUnavailableReason;
|
||||
return (
|
||||
<span className={styles.actionGroup}>
|
||||
<Button
|
||||
@@ -657,11 +665,7 @@ export default function RiskMonitorPage() {
|
||||
{open ? '收起' : '展开'}
|
||||
</Button>
|
||||
<Tooltip
|
||||
title={
|
||||
canAddToWhitelist
|
||||
? undefined
|
||||
: '旧客户端未上报真实设备 ID,无法加入设备白名单'
|
||||
}
|
||||
title={whitelistUnavailableReason}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
|
||||
@@ -15,6 +15,7 @@ export function usePagedList<T>(
|
||||
url: string,
|
||||
filters: Record<string, unknown>,
|
||||
initialPageSize = 20,
|
||||
offsetParam: 'cursor' | 'offset' = 'cursor',
|
||||
) {
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
@@ -31,7 +32,11 @@ export function usePagedList<T>(
|
||||
requestSeq.current = seq;
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = { ...JSON.parse(filtersKey), limit: ps, cursor: (p - 1) * ps };
|
||||
const params = {
|
||||
...JSON.parse(filtersKey),
|
||||
limit: ps,
|
||||
[offsetParam]: (p - 1) * ps,
|
||||
};
|
||||
const { data } = await api.get<CursorPage<T>>(url, { params });
|
||||
if (requestSeq.current !== seq) return; // 丢弃过期响应(快速切页/筛选)
|
||||
setItems(data.items);
|
||||
@@ -40,7 +45,7 @@ export function usePagedList<T>(
|
||||
if (requestSeq.current === seq) setLoading(false);
|
||||
}
|
||||
},
|
||||
[url, filtersKey],
|
||||
[url, filtersKey, offsetParam],
|
||||
);
|
||||
|
||||
// 筛选变化:回第 1 页重载
|
||||
|
||||
Reference in New Issue
Block a user