feat(huya): 协作台账状态机新口径(跳步/已确认绑定/兑换失败)+ 账号全生命周期快照(迁移 0059)

This commit is contained in:
yml2213
2026-10-16 22:00:34 +08:00
parent aac264b639
commit e8abd60734
11 changed files with 1897 additions and 84 deletions
+25
View File
@@ -32,11 +32,14 @@ import type {
HuyaTaskItem,
HuyaBrowserExchangeKit,
HuyaCollabAuditItem,
HuyaCollabActivityBatchResult,
HuyaCollabActivityItem,
HuyaCollabCkPreview,
HuyaCollabCkRequest,
HuyaCollabCkStatusRequest,
HuyaCollabImagesRequest,
HuyaCollabImportResult,
HuyaCollabMetric,
HuyaCollabOrder,
HuyaCollabOrderCreate,
HuyaCollabOrderList,
@@ -47,6 +50,8 @@ import type {
HuyaCollabShopCreate,
HuyaCollabShopUpdate,
HuyaCollabStatusRequest,
HuyaCollabStatusBatchRequest,
HuyaCollabStatusBatchResult,
HuyaCollabSummary,
HuyaCollabVerifyResult,
HuyaExchangeTargetKey,
@@ -252,6 +257,26 @@ export const huyaApi = {
/** 操作时间线(直接读 audit_logs) */
collabOrderTimeline: (id: number) =>
api.get<HuyaCollabAuditItem[], HuyaCollabAuditItem[]>(`/huya/external-orders/${id}/timeline`),
/** 勾选多行一次改业务状态(单个也走这里;回退要带 rollback) */
batchUpdateCollabOrderStatus: (data: HuyaCollabStatusBatchRequest) =>
api.post<HuyaCollabStatusBatchResult, HuyaCollabStatusBatchResult>(
'/huya/external-orders/status/batch',
data,
),
/** 查这一单的积分 + 兑换记录(写一条生命周期快照,已上脚本的单会按结果自动改状态) */
queryCollabOrderActivity: (id: number) =>
api.post<HuyaCollabActivityItem, HuyaCollabActivityItem>(
`/huya/external-orders/${id}/activity/query`,
),
/** 勾选多行查询积分 + 兑换记录(串行带间隔) */
batchQueryCollabOrderActivity: (ids: number[]) =>
api.post<HuyaCollabActivityBatchResult, HuyaCollabActivityBatchResult>(
'/huya/external-orders/activity/query',
{ ids },
),
/** 这一单的「账号全生命周期」时间线(每次查询的快照,新→旧) */
collabOrderMetrics: (id: number) =>
api.get<HuyaCollabMetric[], HuyaCollabMetric[]>(`/huya/external-orders/${id}/metrics`),
/** 删除一行(只给运营:huya:collab:manage) */
deleteCollabOrder: (id: number) =>
api.delete<{ success: boolean; order_no: string }, { success: boolean; order_no: string }>(
+107 -1
View File
@@ -1199,7 +1199,15 @@ export interface AccountExchangeRecords {
// ---- 虎牙外部 CK 协作台账(/api/huya/external-orders,见 docs/设计-虎牙外部CK协作台账.md)----
/** 业务状态:一单卡在哪一步(void 是旁路) */
export type HuyaCollabStatus = 'pending' | 'bound' | 'recharged' | 'scripted' | 'done' | 'void';
export type HuyaCollabStatus =
| 'pending'
| 'bound'
| 'recharged'
| 'scripted'
| 'done'
/** 旁路终态:抢购后查兑换记录没查到这一单的礼包(人工确认后可作废或回退重试) */
| 'failed'
| 'void';
/** 凭据状态:CK 本身还有没有效 */
export type HuyaCollabCkStatus = 'unverified' | 'valid' | 'invalid' | 'disabled';
@@ -1264,6 +1272,12 @@ export interface HuyaCollabOrder {
* 只有真的 ≥2 单才有值,否则是 null。
*/
buyer_cluster: HuyaCollabBuyerCluster | null;
/** 最近一次「查积分 + 兑换记录」的快照(完整时间线走 GET /{id}/metrics) */
last_score: number | null;
last_metric_at: string | null;
last_verdict: HuyaCollabVerdict | '';
last_verdict_label: string;
last_metric_message: string;
}
/** 「同一买家的其它单」里的一条 */
@@ -1288,6 +1302,98 @@ export interface HuyaCollabBuyerCluster {
others: HuyaCollabBuyerOrder[];
}
/** 系统与接口的判定结论(人工不写,只读) */
export type HuyaCollabVerdict = 'success' | 'failed' | 'unknown';
/** 「账号全生命周期」里匹配到的兑换记录 */
export interface HuyaCollabMetricMatched {
prize_name: string;
score: number | null;
status_label: string;
order_id: string;
exchange_time: number | null;
}
/** 一次「查积分 + 兑换记录」的快照(只追加,不更新) */
export interface HuyaCollabMetric {
id: number;
order_id: number;
order_no: string;
uid: string;
sid: number | null;
score: number | null;
gained_score: number | null;
used_score: number | null;
records_total: number;
records_matched: number;
verdict: HuyaCollabVerdict;
verdict_label: string;
message: string;
/** manual(人工点)/ batch(勾选批量) */
source: string;
matched_preview: HuyaCollabMetricMatched[];
created_by: number | null;
created_by_name: string;
created_at: string | null;
}
/** 单个订单查询积分/兑换记录的结果 */
export interface HuyaCollabActivityItem {
ok: boolean;
order_id: number;
order_no: string;
verdict: HuyaCollabVerdict;
verdict_label: string;
score: number | null;
gained_score: number | null;
used_score: number | null;
records_total: number;
records_matched: number;
message: string;
/** 查询后自动改成的状态(空 = 没改:只有「已上脚本」的单会自动改) */
auto_status: HuyaCollabStatus | '';
auto_status_label: string;
status: HuyaCollabStatus;
status_label: string;
}
/** 批量查询汇总 */
export interface HuyaCollabActivityBatchResult {
total: number;
success: number;
failed: number;
unknown: number;
auto_changed: number;
results: HuyaCollabActivityItem[];
}
/** 批量改状态请求(单个也走这里) */
export interface HuyaCollabStatusBatchRequest {
ids: number[];
status: HuyaCollabStatus;
reason?: string;
/** 目标不是「向前」时必须显式 true */
rollback?: boolean;
}
/** 批量改状态里的一行结论 */
export interface HuyaCollabStatusBatchItem {
id: number;
order_no: string;
ok: boolean;
message: string;
}
/** 批量改状态汇总 */
export interface HuyaCollabStatusBatchResult {
total: number;
updated: number;
failed: number;
target: HuyaCollabStatus;
target_label: string;
results: HuyaCollabStatusBatchItem[];
}
export interface HuyaCollabOrderList {
items: HuyaCollabOrder[];
total: number;
+353 -24
View File
@@ -8,15 +8,15 @@
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
Alert, App, AutoComplete, Button, Card, Col, Descriptions, Divider, Drawer, Dropdown, Empty, Form,
Image, Input, Modal, Popconfirm, Popover, Progress, Row, Select, Space, Statistic, Switch, Table,
Tag, Timeline, Tooltip, Typography, Upload,
Alert, App, AutoComplete, Button, Card, Checkbox, Col, Descriptions, Divider, Drawer, Dropdown,
Empty, Form, Image, Input, Modal, Popconfirm, Popover, Progress, Row, Select, Space, Statistic,
Switch, Table, Tag, Timeline, Tooltip, Typography, Upload,
} from 'antd';
import type { TableProps } from 'antd';
import {
CheckCircleOutlined, CloudUploadOutlined, DeleteOutlined, EditOutlined, FileExcelOutlined,
LinkOutlined, PlusOutlined, ReloadOutlined, SafetyCertificateOutlined, ShopOutlined,
StopOutlined, SwapOutlined, UndoOutlined,
StopOutlined, SwapOutlined, SyncOutlined, UndoOutlined,
} from '@ant-design/icons';
import {
fileApi,
@@ -25,6 +25,7 @@ import {
type HuyaCollabCkPreview,
type HuyaCollabGift,
type HuyaCollabImageSlot,
type HuyaCollabMetric,
type HuyaCollabOrder,
type HuyaCollabOrderQuery,
type HuyaCollabOrderUpdate,
@@ -44,13 +45,39 @@ const { Text, Paragraph } = Typography;
const STATUS_META: Record<HuyaCollabStatus, { label: string; color: string }> = {
pending: { label: '待处理', color: 'default' },
bound: { label: '已绑定', color: 'blue' },
// 人工核对过「探针读到的绑定角色」与「主页截图」一致
bound: { label: '已确认绑定', color: 'blue' },
recharged: { label: '已充值', color: 'geekblue' },
scripted: { label: '已上脚本', color: 'purple' },
done: { label: '已完成', color: 'success' },
// 旁路终态:抢购后查兑换记录没查到这一单的礼包
failed: { label: '兑换失败', color: 'red' },
void: { label: '已作废', color: 'error' },
};
/** 判定结论的配色(生命周期时间线用) */
const VERDICT_META: Record<string, { label: string; color: string }> = {
success: { label: '兑换成功', color: 'success' },
failed: { label: '兑换失败', color: 'error' },
unknown: { label: '无法判定', color: 'default' },
};
/** 「改状态」能选的目标(顺序 = 业务流程顺序,回退要显式勾选) */
const STATUS_ORDER_OPTIONS: { value: HuyaCollabStatus; label: string }[] = [
{ value: 'pending', label: '待处理' },
{ value: 'bound', label: '已确认绑定' },
{ value: 'recharged', label: '已充值' },
{ value: 'scripted', label: '已上脚本' },
{ value: 'done', label: '已完成' },
{ value: 'failed', label: '兑换失败' },
{ value: 'void', label: '已作废' },
];
/** 「回退」能选的目标:只在流程链里选(要进「已作废 / 兑换失败」走「改状态」或「作废」入口) */
const FLOW_STATUS_OPTIONS = STATUS_ORDER_OPTIONS.filter(
(option) => option.value !== 'failed' && option.value !== 'void',
);
const CK_META: Record<string, { label: string; color: string }> = {
unverified: { label: '未验证', color: 'default' },
valid: { label: '有效', color: 'success' },
@@ -293,11 +320,21 @@ export default function HuyaCollabPage() {
} | null>(null);
const [ckTarget, setCkTarget] = useState<{ order: HuyaCollabOrder; text: string } | null>(null);
// 单行状态弹窗:set=人工往前切(可跳步)/ rollback=回退 / void=作废
const [statusAction, setStatusAction] = useState<{
order: HuyaCollabOrder; mode: 'void' | 'rollback'; target?: HuyaCollabStatus;
order: HuyaCollabOrder; mode: 'void' | 'rollback' | 'set'; target?: HuyaCollabStatus;
} | null>(null);
const [statusReason, setStatusReason] = useState('');
const [rollbackTarget, setRollbackTarget] = useState<HuyaCollabStatus>('pending');
// 批量改状态(勾选多行一次切)
const [batchStatusOpen, setBatchStatusOpen] = useState(false);
const [batchStatusTarget, setBatchStatusTarget] = useState<HuyaCollabStatus>('bound');
const [batchStatusReason, setBatchStatusReason] = useState('');
const [batchStatusRollback, setBatchStatusRollback] = useState(false);
// 积分 / 兑换记录(「账号全生命周期」)
const [querying, setQuerying] = useState(false);
const [metrics, setMetrics] = useState<HuyaCollabMetric[]>([]);
const [metricsLoading, setMetricsLoading] = useState(false);
const load = useCallback(async () => {
setLoading(true);
@@ -411,6 +448,106 @@ export default function HuyaCollabPage() {
[runCkPreview],
);
/** 勾选多行一次改状态(往前可跳步;回退要在弹窗里勾「回退」) */
const submitBatchStatus = async () => {
setBusy(true);
try {
const result = await huyaApi.batchUpdateCollabOrderStatus({
ids: selectedIds,
status: batchStatusTarget,
reason: batchStatusReason,
rollback: batchStatusRollback,
});
const failures = result.results.filter((item) => !item.ok);
if (failures.length === 0) {
message.success(`已把 ${result.updated} 条改成「${result.target_label}」`);
} else {
modal.info({
title: `改状态完成:成功 ${result.updated} / 未通过 ${result.failed}(目标「${result.target_label}」)`,
width: 640,
content: (
<Space direction="vertical" style={{ width: '100%' }}>
{failures.map((item) => (
<Text key={item.id} type="danger">
{item.order_no}:{item.message}
</Text>
))}
</Space>
),
});
}
setBatchStatusOpen(false);
setSelectedIds([]);
void load();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setBusy(false);
}
};
/** 查这一单的积分 + 兑换记录(写一条生命周期快照;已上脚本的单会按结果自动改状态) */
const queryActivityOne = async (row: HuyaCollabOrder) => {
setQuerying(true);
try {
const result = await huyaApi.queryCollabOrderActivity(row.id);
message[result.verdict === 'success' ? 'success' : result.verdict === 'failed' ? 'warning' : 'info'](
`${row.order_no}:${result.verdict_label} · ${result.message}`
+ (result.auto_status ? `;状态已自动改为「${result.auto_status_label}」` : ''),
);
void load();
if (detail?.id === row.id) {
await openDetail(await huyaApi.getCollabOrder(row.id));
}
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setQuerying(false);
}
};
/** 勾选批量查询(串行带间隔,防风控) */
const queryActivitySelected = async () => {
if (selectedIds.length === 0) return;
setQuerying(true);
try {
const result = await huyaApi.batchQueryCollabOrderActivity(selectedIds);
modal.info({
title: `查询完成:成功 ${result.success} / 失败 ${result.failed} / 无法判定 ${result.unknown}`
+ (result.auto_changed ? `(${result.auto_changed} 条按结果自动改状态)` : ''),
width: 760,
content: (
<Space direction="vertical" style={{ width: '100%' }}>
{result.results.map((item) => (
<Text key={item.order_id} type={item.verdict === 'failed' ? 'danger' : undefined}>
{item.order_no}:{item.verdict_label} · {item.message}
{item.auto_status ? `;状态已改为「${item.auto_status_label}」` : ''}
</Text>
))}
</Space>
),
});
void load();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setQuerying(false);
}
};
/** 详情里的「账号全生命周期」时间线 */
const loadMetrics = async (orderId: number) => {
setMetrics([]);
setMetricsLoading(true);
try {
setMetrics(await huyaApi.collabOrderMetrics(orderId));
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setMetricsLoading(false);
}
};
const patchOrder = async (row: HuyaCollabOrder, payload: HuyaCollabOrderUpdate) => {
try {
const updated = await huyaApi.updateCollabOrder(row.id, { ...payload, expected_version: row.version });
@@ -433,6 +570,7 @@ export default function HuyaCollabPage() {
});
setTimeline([]);
setTimelineLoading(true);
void loadMetrics(row.id);
try {
setTimeline(await huyaApi.collabOrderTimeline(row.id));
} catch (e: unknown) {
@@ -523,22 +661,36 @@ export default function HuyaCollabPage() {
}
};
/**
* 单行状态弹窗的提交(三种模式共用):
* - `void`:作废(必须填原因);
* - `rollback`:回退到选定状态(`rollback=true`,会清掉后面阶段的时间戳);
* - `set`:人工往前切,允许跳步(如 待处理 → 已上脚本),不用勾回退。
*/
const submitStatusAction = async () => {
if (!statusAction) return;
const { order, mode, target } = statusAction;
if (mode === 'void' && !statusReason.trim()) {
const nextStatus: HuyaCollabStatus = mode === 'void' ? 'void' : (target ?? rollbackTarget);
// 服务层对「作废」一律要求原因(不管从哪个入口进来的)
if (nextStatus === 'void' && !statusReason.trim()) {
message.warning('作废必须填原因');
return;
}
setBusy(true);
try {
applyUpdated(await huyaApi.updateCollabOrderStatus(order.id, {
status: mode === 'void' ? 'void' : (target ?? rollbackTarget),
status: nextStatus,
reason: statusReason,
rollback: mode === 'rollback',
expected_version: order.version,
}));
message.success(mode === 'void' ? '已作废' : '已回退');
message.success(
mode === 'void'
? '已作废'
: mode === 'rollback'
? '已回退'
: `已改为「${STATUS_META[nextStatus].label}」`,
);
setStatusAction(null);
setStatusReason('');
void load();
@@ -929,6 +1081,24 @@ export default function HuyaCollabPage() {
</Space>
),
},
{
title: '积分', width: 130,
render: (_, row) => {
if (!row.last_metric_at) {
return <Text type="secondary" style={{ fontSize: 12 }}>还没查</Text>;
}
const verdict = VERDICT_META[row.last_verdict];
return (
<Tooltip title={`${row.last_metric_message || ''}(${formatTime(row.last_metric_at)})`}>
<Space direction="vertical" size={0}>
<Text strong>{row.last_score ?? '-'}</Text>
{verdict && <Tag color={verdict.color} style={{ marginTop: 2 }}>{verdict.label}</Tag>}
<Text type="secondary" style={{ fontSize: 12 }}>{formatTime(row.last_metric_at)}</Text>
</Space>
</Tooltip>
);
},
},
{
title: '最后改动', width: 160,
render: (_, row) => (
@@ -948,10 +1118,12 @@ export default function HuyaCollabPage() {
menu={{
items: [
{ key: 'verify', label: '验证 CK(只读探针)', icon: <SafetyCertificateOutlined /> },
{ key: 'activity', label: '查积分 / 兑换记录', icon: <SyncOutlined />, disabled: !row.has_cookie },
{ key: 'ck', label: '贴 / 换 CK', icon: <LinkOutlined /> },
{ key: 'rebuild', label: '从 App 原始凭据重建', icon: <ReloadOutlined />, disabled: !row.has_raw_cookie },
{ key: 'disable', label: row.ck_status === 'disabled' ? '恢复这份 CK' : '停用这份 CK', icon: <StopOutlined /> },
{ type: 'divider' },
{ key: 'set', label: '改状态…', icon: <CheckCircleOutlined /> },
{ key: 'rollback', label: '回退状态…', icon: <UndoOutlined /> },
{ key: 'void', label: '作废…', icon: <StopOutlined />, danger: true },
...(canManage
@@ -960,9 +1132,15 @@ export default function HuyaCollabPage() {
],
onClick: ({ key }) => {
if (key === 'verify') void verifyOne(row);
if (key === 'activity') void queryActivityOne(row);
if (key === 'ck') setCkTarget({ order: row, text: '' });
if (key === 'rebuild') setCkTarget({ order: row, text: '' });
if (key === 'disable') void toggleCkDisabled(row);
if (key === 'set') {
setRollbackTarget(row.next_status ?? row.status);
setStatusReason('');
setStatusAction({ order: row, mode: 'set' });
}
if (key === 'rollback') {
setRollbackTarget('pending');
setStatusReason('');
@@ -993,32 +1171,40 @@ export default function HuyaCollabPage() {
return (
<Space direction="vertical" size={12} style={{ width: '100%' }}>
{/* 卡片用 flex 平分:状态多了一个「兑换失败」,固定 span=4 会换行参差不齐 */}
<Row gutter={12}>
<Col span={4}><Card size="small"><Statistic title="全部" value={summary?.total ?? 0} /></Card></Col>
<Col span={4}>
<Col flex="1"><Card size="small"><Statistic title="全部" value={summary?.total ?? 0} /></Card></Col>
<Col flex="1">
<Card size="small">
<Statistic title="待处理" value={summary?.by_status.pending ?? 0} valueStyle={{ color: '#faad14' }} />
</Card>
</Col>
<Col span={4}>
<Col flex="1">
<Card size="small">
<Statistic title="已绑定" value={summary?.by_status.bound ?? 0} valueStyle={{ color: '#1677ff' }} />
<Statistic title={STATUS_META.bound.label} value={summary?.by_status.bound ?? 0}
valueStyle={{ color: '#1677ff' }} />
</Card>
</Col>
<Col span={4}>
<Col flex="1">
<Card size="small">
<Statistic title="已充值 / 已上脚本"
value={(summary?.by_status.recharged ?? 0) + (summary?.by_status.scripted ?? 0)}
valueStyle={{ color: '#722ed1' }} />
</Card>
</Col>
<Col span={4}>
<Col flex="1">
<Card size="small">
<Statistic title="已完成" value={finished} valueStyle={{ color: '#52c41a' }} />
<Progress percent={summary?.total ? Math.round((finished / summary.total) * 100) : 0} size="small" />
</Card>
</Col>
<Col span={4}>
<Col flex="1">
<Card size="small">
<Statistic title={STATUS_META.failed.label} value={summary?.by_status.failed ?? 0}
valueStyle={{ color: '#ff4d4f' }} />
</Card>
</Col>
<Col flex="1">
<Card size="small">
<Statistic title="CK 失效" value={summary?.ck_invalid ?? 0} valueStyle={{ color: '#ff4d4f' }} />
</Card>
@@ -1068,6 +1254,35 @@ export default function HuyaCollabPage() {
批量验证 CK{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
</Button>
)}
{canEdit && (
// 勾选多行一次改状态(往前可跳步;回退在弹窗里勾「回退」)
<Dropdown
disabled={selectedIds.length === 0 || busy}
menu={{
items: STATUS_ORDER_OPTIONS.map((option) => ({
key: option.value,
label: option.label,
})),
onClick: ({ key }) => {
setBatchStatusTarget(key as HuyaCollabStatus);
setBatchStatusReason('');
setBatchStatusRollback(false);
setBatchStatusOpen(true);
},
}}
>
<Button icon={<SwapOutlined />} disabled={selectedIds.length === 0 || busy}>
批量改状态{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
</Button>
</Dropdown>
)}
{canEdit && (
<Button icon={<SyncOutlined />} disabled={selectedIds.length === 0 || busy}
loading={querying}
onClick={() => void queryActivitySelected()}>
批量查积分/兑换{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
</Button>
)}
{canEdit && (
<Button icon={<FileExcelOutlined />} onClick={() => { setImportResult(null); setImportOpen(true); }}>
粘贴导入
@@ -1280,13 +1495,22 @@ export default function HuyaCollabPage() {
)}
{canEdit && (
<Space>
<Space wrap>
{detail.next_status && (
<Button icon={<CheckCircleOutlined />} disabled={busy}
onClick={() => void advance(detail)}>
标记为「{STATUS_META[detail.next_status].label}」
</Button>
)}
<Button icon={<SwapOutlined />} disabled={busy}
onClick={() => {
// 默认给「下一步」,终点态(已完成 / 已作废 / 兑换失败)退回当前状态,让人自己挑
setRollbackTarget(detail.next_status ?? detail.status);
setStatusReason('');
setStatusAction({ order: detail, mode: 'set' });
}}>
改状态…
</Button>
<Button icon={<UndoOutlined />} disabled={busy}
onClick={() => { setRollbackTarget('pending'); setStatusReason(''); setStatusAction({ order: detail, mode: 'rollback' }); }}>
回退状态
@@ -1321,6 +1545,68 @@ export default function HuyaCollabPage() {
),
}))}
/>
<Divider style={{ margin: '4px 0' }}>账号全生命周期(积分 / 兑换记录)</Divider>
<Space wrap size={8}>
{canEdit && (
<Button icon={<SyncOutlined />} disabled={busy || querying || !detail.has_cookie}
loading={querying}
onClick={() => void queryActivityOne(detail)}>
查一次积分 / 兑换记录
</Button>
)}
<Text type="secondary" style={{ fontSize: 12 }}>
每次查询都会追加一条快照(只追加、不修改);「已上脚本」的单会按结论自动改成「已完成」或「兑换失败」。
</Text>
</Space>
{!detail.has_cookie && (
<Text type="warning" style={{ fontSize: 12 }}>这单还没贴 CK,查不了积分 / 兑换记录。</Text>
)}
{metrics.length === 0 && !metricsLoading && (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="还没有查询快照" />
)}
<Timeline
pending={metricsLoading ? '查询中…' : undefined}
items={metrics.map((item) => ({
color: item.verdict === 'success' ? 'green' : item.verdict === 'failed' ? 'red' : 'gray',
children: (
<Space direction="vertical" size={2} style={{ width: '100%' }}>
<Space size={6} wrap>
<Tag color={VERDICT_META[item.verdict]?.color}>{item.verdict_label}</Tag>
<Text strong>积分 {item.score ?? '-'}</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
获得 {item.gained_score ?? '-'} / 消耗 {item.used_score ?? '-'}
{' · '}兑换记录 {item.records_total} 条(命中 {item.records_matched})
</Text>
</Space>
{item.message && (
<Text type={item.verdict === 'failed' ? 'danger' : 'secondary'} style={{ fontSize: 12 }}>
{item.message}
</Text>
)}
{item.matched_preview.length > 0 && (
<Space direction="vertical" size={0}>
{item.matched_preview.map((matched, index) => (
<Text key={`${matched.order_id}-${index}`} style={{ fontSize: 12 }}>
<Text code style={{ fontSize: 12 }}>{matched.prize_name || '(无礼包名)'}</Text>
{' '}
<Text type="secondary" style={{ fontSize: 12 }}>
{matched.status_label}
{matched.order_id ? ` · 兑换单 ${matched.order_id}` : ''}
{matched.exchange_time ? ` · ${formatTime(new Date(matched.exchange_time * 1000).toISOString())}` : ''}
</Text>
</Text>
))}
</Space>
)}
<Text type="secondary" style={{ fontSize: 12 }}>
{formatTime(item.created_at)} · {item.created_by_name || '系统'}
{' · '}{item.source === 'batch' ? '批量查询' : '手动查询'}
</Text>
</Space>
),
}))}
/>
</Space>
)}
</Drawer>
@@ -1576,34 +1862,77 @@ export default function HuyaCollabPage() {
<Modal
title={statusAction?.mode === 'void'
? `作废订单 ${statusAction.order.order_no}`
: `回退订单 ${statusAction?.order.order_no ?? ''}`}
: statusAction?.mode === 'set'
? `改状态:订单 ${statusAction.order.order_no}`
: `回退订单 ${statusAction?.order.order_no ?? ''}`}
open={!!statusAction}
onCancel={() => setStatusAction(null)}
onOk={() => void submitStatusAction()}
confirmLoading={busy}
okButtonProps={{ danger: statusAction?.mode === 'void' }}
okText={statusAction?.mode === 'void' ? '作废' : '回退'}
okText={statusAction?.mode === 'void' ? '作废' : statusAction?.mode === 'set' ? '改状态' : '回退'}
destroyOnHidden
>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
{statusAction?.mode === 'rollback' && (
{statusAction && statusAction.mode !== 'void' && (
<Select
style={{ width: '100%' }}
value={rollbackTarget}
onChange={(value: HuyaCollabStatus) => setRollbackTarget(value)}
options={(['pending', 'bound', 'recharged', 'scripted'] as HuyaCollabStatus[]).map((key) => ({
value: key, label: STATUS_META[key].label,
}))}
options={statusAction.mode === 'set' ? STATUS_ORDER_OPTIONS : FLOW_STATUS_OPTIONS}
/>
)}
<Input.TextArea rows={2} value={statusReason} onChange={(event) => setStatusReason(event.target.value)}
placeholder={statusAction?.mode === 'void' ? '作废原因(必填)' : '回退说明(可选)'} />
placeholder={
statusAction?.mode === 'void'
? '作废原因(必填)'
: rollbackTarget === 'void'
? '作废原因(必填)'
: statusAction?.mode === 'rollback'
? '回退说明(可选)'
: '改状态说明(可选)'
} />
{statusAction?.mode === 'set' && (
<Text type="secondary">
人工切换允许往前跳步(比如 待处理 → 已上脚本),不用一步一步点;
切到「已确认绑定」要求先有绑定角色 + 主页截图,切到「已作废」必须填原因。
</Text>
)}
{statusAction?.mode === 'rollback' && (
<Text type="secondary">回退会清掉后面阶段的时间戳,并在审计里留痕。</Text>
)}
</Space>
</Modal>
<Modal
title={`批量改状态(已选 ${selectedIds.length} 条)`}
open={batchStatusOpen}
onCancel={() => setBatchStatusOpen(false)}
onOk={() => void submitBatchStatus()}
confirmLoading={busy}
okText={`改成「${STATUS_META[batchStatusTarget]?.label ?? ''}」`}
destroyOnHidden
>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<Select
style={{ width: '100%' }}
value={batchStatusTarget}
onChange={(value: HuyaCollabStatus) => setBatchStatusTarget(value)}
options={STATUS_ORDER_OPTIONS}
/>
<Input.TextArea rows={2} value={batchStatusReason}
onChange={(event) => setBatchStatusReason(event.target.value)}
placeholder={batchStatusTarget === 'void' ? '作废原因(必填)' : '改状态说明(可选)'} />
<Checkbox checked={batchStatusRollback} onChange={(event) => setBatchStatusRollback(event.target.checked)}>
回退(目标不是「往前」时必须勾选,比如 已上脚本 → 待处理)
</Checkbox>
<Text type="secondary">
逐行独立提交:某一行前置校验没过(例如没主页截图进不了「已确认绑定」)只跳过那一行,
结果会在弹窗里逐条列出来。一次最多 50 行。
</Text>
</Space>
</Modal>
<HuyaCollabShopModal
open={shopModalOpen}
shops={shops}