feat(douyu): 高价值套装改用官方 storehome 三态判定
高价值套装的准入判定改为只认官方精英宝典 storehome 里账号级的 status:0=可兑换、1=已达兑换上限,商品缺失/接口异常=unknown。 兑换记录和积分记录只作历史展示,不参与是否拥有的判定。 原来的 high_value_owned(JSON 名单)无法区分「确认可兑换」和 「官方接口没返回商品」,也无法表达部分失败。改为两套独立状态 + 检查时间(迁移 20261106_0065),旧快照保守回填:status 缺失的 一律 unknown,不猜。 CK 判定失效时在数据层作废结论(降为 unknown),不是展示层降级。 官方 status 是账号维度的,但 CK 失效那段时间账号可能被别人手工 登录并兑换(这些号共享给客服),缓存里的「可兑换」就不成立了; 只在展示层降级的话,CK 恢复后那条陈旧的「可换」会重新变绿。 error(查不到)不作废 —— 查不到不等于账号出问题了。 高价值结论有自己的时钟:资料刷新除了看 overview_queried_at,还看 high_value_checked_at。两者会脱钩(商品接口单独失败时资料时间被刷新、 高价值时间停在原地),少了这一条这类账号会永久停在旧结论。作废的 结论按 6 小时慢速重试,但「从来没查到过」不因此重选 —— 那是接口对 该账号稳定失败,每轮都重选只会反复撞墙。 商城商品列表改用精英宝典的 manualID 拉取,不再寄生在电竞手册分支里 (否则没配电竞手册的配置就永远拉不到商城,三态全部停在待确认)。 筛选拆成三类「查不到」:unqueried(未查资料,旧口径不变)、 high_value_never(从未读到商城)、high_value_unknown(查过但结论 未知/过期)。之前第二类无处可寻 —— 它既不满足 unqueried 也不满足 high_value_unknown。
This commit is contained in:
@@ -98,10 +98,31 @@ export interface HighValueSummary {
|
||||
can_bairimeng: number;
|
||||
can_mengtu: number;
|
||||
unqueried: number;
|
||||
high_value_unknown: number;
|
||||
high_value_never: number;
|
||||
high_points: number;
|
||||
has_gold: number;
|
||||
elite_opened: number;
|
||||
high_value_broken: number;
|
||||
owned_bairimeng: number;
|
||||
owned_mengtu: number;
|
||||
both_owned: number;
|
||||
}
|
||||
|
||||
export type HighValueAssetState = 'owned' | 'available' | 'unknown' | '';
|
||||
export type HighValueQueryState = 'never' | 'partial' | 'stale' | 'complete';
|
||||
|
||||
export interface HighValueStatus {
|
||||
state: HighValueQueryState;
|
||||
checked_at: string | null;
|
||||
stale: boolean;
|
||||
fresh: boolean;
|
||||
bairimeng: HighValueAssetState;
|
||||
mengtu: HighValueAssetState;
|
||||
names: {
|
||||
bairimeng: string;
|
||||
mengtu: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BasicSummary {
|
||||
@@ -222,8 +243,8 @@ export interface AccountOverview {
|
||||
stats: AccountOverviewStats;
|
||||
/** 最近一次「一键查询」的快照(结构与后端 query_account_overview 的 result 一致) */
|
||||
overview_snapshot: import('../pages/douyu-tasks/AccountOverviewPanel').AccountOverviewResult | null;
|
||||
/** 一键查询得到的已兑换高价值套装,为 null 时表示未查询 */
|
||||
high_value_owned: string[] | null;
|
||||
/** 官方 storehome 的三态准入,不知道时绝不等于可兑换 */
|
||||
high_value: HighValueStatus;
|
||||
overview_queried_at: string | null;
|
||||
/** 开通时间来自事件表(上线后才开始记录,更早的不显示) */
|
||||
opened_at: { elite: string | null; esports: string | null };
|
||||
@@ -374,9 +395,9 @@ export interface AccountItem {
|
||||
exchange_count: number;
|
||||
last_exchange_at: string | null;
|
||||
last_exchange_name: string;
|
||||
/** 一键查询得到的「已兑换过的高价值套装」,为 null 时表示未查询 */
|
||||
high_value_owned: string[] | null;
|
||||
/** 最近一次一键查询时间 —— 用来区分「查过但没兑换」和「还没查过」 */
|
||||
/** 官方 storehome 的三态准入 */
|
||||
high_value: HighValueStatus;
|
||||
/** 最近一次完整一键查询时间 */
|
||||
overview_queried_at: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { TableProps } from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined, EditOutlined, ReloadOutlined, ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { accountApi, douyuApi, userApi, type AccountEventItem, type AccountOverview, type UserInfo } from '../api/modules';
|
||||
import { accountApi, douyuApi, userApi, type AccountEventItem, type AccountOverview, type HighValueAssetState, type HighValueStatus, type UserInfo } from '../api/modules';
|
||||
import AccountOverviewPanel from './douyu-tasks/AccountOverviewPanel';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
import { message } from '../utils/antdMessage';
|
||||
@@ -64,6 +64,33 @@ const BIND_TONES: Record<string, string> = {
|
||||
unknown: 'default',
|
||||
};
|
||||
|
||||
function highValueShortTag(label: string, status: HighValueAssetState) {
|
||||
if (status === 'owned') return <Tag color="orange">{label}已达上限</Tag>;
|
||||
if (status === 'available') return <Tag color="green">{label}可换</Tag>;
|
||||
return <Tag>{label}待确认</Tag>;
|
||||
}
|
||||
|
||||
function CurrentHighValue({ value }: { value: HighValueStatus }) {
|
||||
if (value.state === 'never') {
|
||||
return <Text type="secondary">尚未成功获取官方商城准入状态</Text>;
|
||||
}
|
||||
if (value.state === 'stale') {
|
||||
return (
|
||||
<Tooltip title={`上次检查:${formatTime(value.checked_at)}`}>
|
||||
<Tag>官方状态已过期,需要重新查询</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip title={`官方 storehome 检查于 ${formatTime(value.checked_at)}`}>
|
||||
<Space wrap size={4}>
|
||||
{highValueShortTag('白日梦', value.bairimeng)}
|
||||
{highValueShortTag('萌兔', value.mengtu)}
|
||||
</Space>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function eventTag(type: string) {
|
||||
const style = EVENT_STYLE[type] ?? { label: type, color: 'default' };
|
||||
return <Tag color={style.color}>{style.label}</Tag>;
|
||||
@@ -665,6 +692,14 @@ export default function AccountDetailPage() {
|
||||
{ovError}
|
||||
</Text>
|
||||
)}
|
||||
{overview?.high_value && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<Text type="secondary" style={{ marginRight: 8 }}>
|
||||
当前官方准入:
|
||||
</Text>
|
||||
<CurrentHighValue value={overview.high_value} />
|
||||
</div>
|
||||
)}
|
||||
{overview?.overview_snapshot ? (
|
||||
<AccountOverviewPanel result={overview.overview_snapshot} />
|
||||
) : (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { TableProps } from 'antd';
|
||||
import { Space, Button, Typography, Tag, Tooltip, Select, Popconfirm } from 'antd';
|
||||
import { CopyOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import type { NavigateFunction } from 'react-router-dom';
|
||||
import type { AccountItem } from '../../../api/modules';
|
||||
import type { AccountItem, HighValueAssetState, HighValueStatus } from '../../../api/modules';
|
||||
import { message } from '../../../utils/antdMessage';
|
||||
import { copyTextToClipboard } from '../../../utils/clipboard';
|
||||
import { formatTimeShort } from '../../../utils/time';
|
||||
@@ -39,6 +39,49 @@ function BindChip({ scope, status }: { scope: string; status?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function highValueAssetTag(
|
||||
shortName: string,
|
||||
state: HighValueAssetState,
|
||||
) {
|
||||
if (state === 'owned') {
|
||||
return <Tag color="orange" style={{ marginInlineEnd: 0 }}>{shortName}已换</Tag>;
|
||||
}
|
||||
if (state === 'available') {
|
||||
return <Tag color="green" style={{ marginInlineEnd: 0 }}>{shortName}可换</Tag>;
|
||||
}
|
||||
return <Tag style={{ marginInlineEnd: 0 }}>{shortName}待确认</Tag>;
|
||||
}
|
||||
|
||||
function renderHighValueStatus(highValue: HighValueStatus | undefined) {
|
||||
if (!highValue || highValue.state === 'never') {
|
||||
return (
|
||||
<Tooltip title="尚未成功读取官方商城准入状态,点工具栏「批量一键查询」">
|
||||
<Text type="secondary">未查询</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if (highValue.state === 'stale') {
|
||||
return (
|
||||
<Tooltip
|
||||
title={`官方商城结果已过期,上次检查:${formatTimeShort(highValue.checked_at)}`}
|
||||
>
|
||||
<Tag>数据已过期</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
const checked = highValue.checked_at
|
||||
? `官方 storehome 检查于 ${formatTimeShort(highValue.checked_at)}`
|
||||
: '官方 storehome 状态';
|
||||
return (
|
||||
<Tooltip title={checked}>
|
||||
<Space size={4} wrap>
|
||||
{highValueAssetTag('白日梦', highValue.bairimeng)}
|
||||
{highValueAssetTag('萌兔', highValue.mengtu)}
|
||||
</Space>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export interface UseAccountColumnsOptions {
|
||||
navigate: NavigateFunction;
|
||||
sortKey: AccountSortKey | '';
|
||||
@@ -270,39 +313,23 @@ export function buildAccountColumns({
|
||||
},
|
||||
{
|
||||
title: '高价值套装',
|
||||
dataIndex: 'high_value_owned',
|
||||
dataIndex: 'high_value',
|
||||
...sortProps('high_value', '两个都可兑换在前,未查询在后'),
|
||||
width: 170,
|
||||
render: (owned: string[] | null | undefined, record: AccountItem) => {
|
||||
if (owned === null || owned === undefined || !record.overview_queried_at) {
|
||||
return (
|
||||
<Tooltip title="尚未查询高价值套装准入状态,点工具栏「批量一键查询」">
|
||||
<Text type="secondary">未查询</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if (owned.length === 0) {
|
||||
return <Tag color="green">两个都可兑换</Tag>;
|
||||
}
|
||||
return (
|
||||
<Space size={4} wrap>
|
||||
{owned.map((n) => (
|
||||
<Tag key={n} color="orange">
|
||||
{n.replace(/^套装-/, '')} 已达上限
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
width: 190,
|
||||
render: (_, record: AccountItem) => renderHighValueStatus(record.high_value),
|
||||
},
|
||||
{
|
||||
title: '兑换记录',
|
||||
title: '官方兑换记录',
|
||||
dataIndex: 'exchange_count',
|
||||
...sortProps('exchange', '未兑换在前'),
|
||||
...sortProps('exchange', '没有官方记录在前'),
|
||||
width: 170,
|
||||
render: (_, record: AccountItem) => {
|
||||
if (!record.exchange_count) {
|
||||
return <Text type="secondary">未兑换</Text>;
|
||||
return (
|
||||
<Tooltip title="官方兑换列表当前没有记录;官方会清理历史,不能据此推断账号没换过">
|
||||
<Text type="secondary">无官方记录</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tooltip title="点击查看全部官方兑换记录">
|
||||
|
||||
@@ -217,6 +217,58 @@ export function FilterPanel({
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'high_value_unknown',
|
||||
label: '高价值待确认',
|
||||
icon: '❓',
|
||||
count: summary.high_value?.high_value_unknown,
|
||||
color: '#8c8c8c',
|
||||
tooltip: '官方商城未返回套装、查询部分失败,或上次结果已超过 7 天',
|
||||
active:
|
||||
highValueFilter === 'high_value_unknown' &&
|
||||
minPoints === undefined &&
|
||||
maxPoints === undefined &&
|
||||
minGold === undefined &&
|
||||
maxGold === undefined &&
|
||||
eliteOpened === undefined &&
|
||||
esportsOpened === undefined &&
|
||||
!roleBindStatus,
|
||||
onClick: () => {
|
||||
if (highValueFilter === 'high_value_unknown') {
|
||||
setHighValueFilter('');
|
||||
} else {
|
||||
onResetAdvancedFiltersState();
|
||||
setHighValueFilter('high_value_unknown');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'high_value_never',
|
||||
label: '未读到商城',
|
||||
icon: '❔',
|
||||
count: summary.high_value?.high_value_never,
|
||||
color: '#8c8c8c',
|
||||
tooltip: '从未成功读到官方商城准入状态:商城接口失败,或未配置精英宝典 manualID',
|
||||
active:
|
||||
highValueFilter === 'high_value_never' &&
|
||||
minPoints === undefined &&
|
||||
maxPoints === undefined &&
|
||||
minGold === undefined &&
|
||||
maxGold === undefined &&
|
||||
eliteOpened === undefined &&
|
||||
esportsOpened === undefined &&
|
||||
!roleBindStatus,
|
||||
onClick: () => {
|
||||
if (highValueFilter === 'high_value_never') {
|
||||
setHighValueFilter('');
|
||||
} else {
|
||||
onResetAdvancedFiltersState();
|
||||
setHighValueFilter('high_value_never');
|
||||
}
|
||||
setCurrentPage(1);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'high_points',
|
||||
label: '积分≥300',
|
||||
|
||||
@@ -82,6 +82,8 @@ export const HIGH_VALUE_OPTIONS = [
|
||||
{ value: 'owned_mengtu', label: '✓ 已拥有萌兔' },
|
||||
{ value: 'both_owned', label: '✓ 两套均已拥有' },
|
||||
{ value: 'high_value_broken', label: '⚠️ 高价值但CK失效' },
|
||||
{ value: 'high_value_unknown', label: '❓ 高价值待确认/已过期' },
|
||||
{ value: 'high_value_never', label: '❓ 从未读到官方商城状态' },
|
||||
{ value: 'unqueried', label: '❓ 未查过资料' },
|
||||
];
|
||||
|
||||
|
||||
@@ -5,22 +5,27 @@ const { Text } = Typography;
|
||||
|
||||
/** 一键查询的结果结构(后端 `query_account_overview` 写入 task.result)。 */
|
||||
export interface HighValueGoods {
|
||||
key?: 'bairimeng' | 'mengtu';
|
||||
name: string;
|
||||
commodity_id?: string;
|
||||
score?: number | null;
|
||||
count?: number | null;
|
||||
status?: number | string | null;
|
||||
store_status?: number | null;
|
||||
count_limited?: number | null;
|
||||
start_time?: string | number | null;
|
||||
end_time?: string | number | null;
|
||||
pic?: string;
|
||||
/** **是否已获得** —— 依据是斗鱼官方的账号级兑换记录,不是全局库存 */
|
||||
/** 是否已达兑换上限 —— 唯一依据是官方 storehome.status */
|
||||
owned?: boolean | null;
|
||||
available?: boolean | null;
|
||||
/** owned / available / unknown */
|
||||
state?: 'owned' | 'available' | 'unknown';
|
||||
/** 官方兑换记录里命中的商品名(可能是「破片手榴弹-兔兔白日梦」这类变体) */
|
||||
obtained_names?: string[];
|
||||
/** 该商品当前是否在限兑列表里(全局信息,与"是否已获得"无关) */
|
||||
in_goods_list?: boolean;
|
||||
/** 限兑列表和兑换记录里都没有这个商品 */
|
||||
/** 官方商城本次没有返回这个商品 */
|
||||
missing?: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
@@ -67,6 +72,7 @@ const ERROR_LABELS: Record<string, string> = {
|
||||
points: '积分',
|
||||
elite: '精英宝典',
|
||||
esports: '电竞手册',
|
||||
limited_goods: '官方商城商品列表',
|
||||
high_value_goods: '高价值套装',
|
||||
exchange_records: '兑换记录',
|
||||
};
|
||||
@@ -97,13 +103,6 @@ function formatTime(value: string | number | null | undefined): string {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 高价值套装卡片。
|
||||
*
|
||||
* ⚠️ 措辞刻意不用「已拥有」:`storeStatus=0` 也可能是**全局售罄/活动下架**,
|
||||
* 我们没法只凭它断言账号真的拿到了。所以写「不可再兑」并把 count 一起显示,
|
||||
* 让运营自己判断 —— 这比给一个可能错的结论好。
|
||||
*/
|
||||
/**
|
||||
* 高价值套装的展示状态。
|
||||
*
|
||||
@@ -112,19 +111,13 @@ function formatTime(value: string | number | null | undefined): string {
|
||||
* 兑换过 → 「已兑换」**橙色**(价值降低)
|
||||
* 同一件东西两个都兑换过,账号价值最低。
|
||||
*
|
||||
* 「是否已获得」只看**官方兑换记录**:`count`/`store_status` 是全局库存,
|
||||
* 同一时刻不同账号看到的都一样,不能用来判断这个账号有没有。
|
||||
* 当前准入只看官方 storehome.status:1=已达上限、0=可兑换。
|
||||
* count/foreverCount/storeStatus 是全局库存或商店状态,不能判断账号是否已换。
|
||||
*/
|
||||
function goodsState(g: HighValueGoods): { text: string; color: string } {
|
||||
if (g.missing) return { text: '未查到该商品', color: 'default' };
|
||||
if (g.owned) {
|
||||
const isLimit = g.reason?.includes('已达兑换上限');
|
||||
return {
|
||||
text: isLimit ? '已达上限(不可兑)' : '已兑换(价值降低)',
|
||||
color: 'orange',
|
||||
};
|
||||
}
|
||||
return { text: '可兑换', color: 'green' };
|
||||
if (g.state === 'owned') return { text: '已达上限(不可兑)', color: 'orange' };
|
||||
if (g.state === 'available') return { text: '可兑换', color: 'green' };
|
||||
return { text: '状态未知', color: 'default' };
|
||||
}
|
||||
|
||||
const goodsColumns: ColumnsType<HighValueGoods> = [
|
||||
@@ -135,7 +128,7 @@ const goodsColumns: ColumnsType<HighValueGoods> = [
|
||||
width: 200,
|
||||
render: (name: string, g) =>
|
||||
g.missing ? (
|
||||
<Tooltip title={g.reason || '商品列表和官方兑换记录里都没有它'}>
|
||||
<Tooltip title={g.reason || '官方商城本次没有返回该商品'}>
|
||||
<Text type="secondary">{name}</Text>
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -162,8 +155,11 @@ const goodsColumns: ColumnsType<HighValueGoods> = [
|
||||
key: 'reason',
|
||||
width: 220,
|
||||
render: (_, g) => (
|
||||
<Text type={g.owned ? 'warning' : 'secondary'} style={{ fontSize: 13 }}>
|
||||
{g.reason || (g.owned ? '官方限制兑换' : '官方商城可正常兑换')}
|
||||
<Text
|
||||
type={g.state === 'owned' ? 'warning' : 'secondary'}
|
||||
style={{ fontSize: 13 }}
|
||||
>
|
||||
{g.reason || (g.state === 'available' ? '官方商城可正常兑换' : '等待官方状态')}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user