fix(overview): 锁定核心高价值套装并修复未查询空值误判

- 高价值套装精确匹配「套装-兔兔白日梦」与「套装-狂战萌兔」,排除头盔/背包/手榴弹散件干扰
- 增加 is_suit_queried_expr,未查询或 NULL 账号严禁误判为双套均可兑换
- 修复前后端序列化空值保持,完善列表与详情页准入状态和判定依据展示
- 新增头盔排除与 NULL 统计防错回归单测,全量通过 689 个用例
This commit is contained in:
yml2213
2026-10-07 15:27:30 +08:00
parent 5405d4b1d1
commit 44b762ea8c
11 changed files with 202 additions and 112 deletions
+4 -4
View File
@@ -211,8 +211,8 @@ export interface AccountOverview {
stats: AccountOverviewStats;
/** 最近一次「一键查询」的快照(结构与后端 query_account_overview 的 result 一致) */
overview_snapshot: import('../pages/douyu-tasks/AccountOverviewPanel').AccountOverviewResult | null;
/** 一键查询得到的已兑换高价值套装 */
high_value_owned: string[];
/** 一键查询得到的已兑换高价值套装,为 null 时表示未查询 */
high_value_owned: string[] | null;
overview_queried_at: string | null;
/** 开通时间来自事件表(上线后才开始记录,更早的不显示) */
opened_at: { elite: string | null; esports: string | null };
@@ -315,8 +315,8 @@ export interface AccountItem {
exchange_count: number;
last_exchange_at: string | null;
last_exchange_name: string;
/** 一键查询得到的「已兑换过的高价值套装」(账号表里没有,来自官方兑换记录) */
high_value_owned: string[];
/** 一键查询得到的「已兑换过的高价值套装」,为 null 时表示未查询 */
high_value_owned: string[] | null;
/** 最近一次一键查询时间 —— 用来区分「查过但没兑换」和「还没查过」 */
overview_queried_at: string | null;
}
+8 -11
View File
@@ -756,27 +756,24 @@ export default function AccountsPage() {
dataIndex: 'high_value_owned',
...sortProps('high_value', '两个都可兑换在前,未查询在后'),
width: 170,
render: (owned: string[] | undefined, record: AccountItem) => {
const used = owned ?? [];
// 没查过 ≠ 没兑换:必须分开说,否则会把"没数据"当成"高价值"
// 「有没有查过」看查询时间:只看 owned 是否为空的话,
// 「查过但没兑换」(高价值)和「没查过」(未知)就分不开了。
const queried = Boolean(record.overview_queried_at);
if (!queried) {
render: (owned: string[] | null | undefined, record: AccountItem) => {
// 没查过 ≠ 没兑换:
// 只有 high_value_owned 明确为数组且有查询时间时,才算有效查过高价值
if (owned === null || owned === undefined || !record.overview_queried_at) {
return (
<Tooltip title="还没做过一键查询,点工具栏「批量一键查询」">
<Tooltip title="尚未查询高价值套装准入状态,点工具栏「批量一键查询」">
<Text type="secondary">未查询</Text>
</Tooltip>
);
}
if (used.length === 0) {
if (owned.length === 0) {
return <Tag color="green">两个都可兑换</Tag>;
}
return (
<Space size={4} wrap>
{used.map((n) => (
{owned.map((n) => (
<Tag key={n} color="orange">
{n.replace(/^套装-/, '')} 已兑换
{n.replace(/^套装-/, '')} 已达上限
</Tag>
))}
</Space>
@@ -145,9 +145,9 @@ const goodsColumns: ColumnsType<HighValueGoods> = [
),
},
{
title: '可兑换 / 已兑换',
title: '准入状态',
key: 'owned',
width: 150,
width: 160,
render: (_, g) => {
const state = goodsState(g);
return (
@@ -158,21 +158,21 @@ const goodsColumns: ColumnsType<HighValueGoods> = [
},
},
{
title: '兑换记录里的名字',
key: 'obtained',
render: (_, g) =>
g.obtained_names?.length ? (
<Text style={{ fontSize: 12 }}>{g.obtained_names.join('、')}</Text>
) : (
<Text type="secondary">—</Text>
),
title: '判定依据',
key: 'reason',
width: 220,
render: (_, g) => (
<Text type={g.owned ? 'warning' : 'secondary'} style={{ fontSize: 13 }}>
{g.reason || (g.owned ? '官方限制兑换' : '官方商城可正常兑换')}
</Text>
),
},
{
title: '积分价',
dataIndex: 'score',
key: 'score',
width: 90,
render: (v: number | null | undefined) => v ?? '—',
render: (v: number | null | undefined) => (v != null ? `${v} 分` : '—'),
},
{
title: '当前库存',
@@ -193,7 +193,7 @@ const goodsColumns: ColumnsType<HighValueGoods> = [
{
title: '活动时间',
key: 'window',
width: 200,
width: 180,
render: (_, g) =>
g.missing ? '—' : `${formatTime(g.start_time)} ~ ${formatTime(g.end_time)}`,
},
@@ -280,7 +280,7 @@ export default function AccountOverviewPanel({
<Card
size="small"
title="🎯 高价值套装(没兑换过才值钱)" styles={{ body: { padding: 0 } }}>
title="🎯 高价值套装准入(核心关注:套装-兔兔白日梦、套装-狂战萌兔)" styles={{ body: { padding: 0 } }}>
{goods.length === 0 ? (
<div style={{ padding: 12 }}>
<Text type="secondary">没有配置关注的高价值套装,或本次没取到</Text>