feat(weibo): 优化协作单详情并增加已上脚本状态
This commit is contained in:
@@ -29,14 +29,13 @@ from web.backend.models import (
|
||||
HuyaExternalOrder,
|
||||
User,
|
||||
WeiboAccount,
|
||||
WeiboExternalOrder,
|
||||
)
|
||||
|
||||
TESTS_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = TESTS_DIR.parent
|
||||
VERSIONS_DIR = PROJECT_ROOT / "web" / "backend" / "migrations" / "versions"
|
||||
|
||||
HEAD_REVISION = "20261112_0079"
|
||||
HEAD_REVISION = "20261112_0080"
|
||||
|
||||
# 迁移新增、但不声明在模型里的复合查询索引。
|
||||
EXTRA_INDEXES = (
|
||||
@@ -441,14 +440,16 @@ class TestMigrationSmoke:
|
||||
cfg.set_main_option("sqlalchemy.url", mig_url)
|
||||
command.upgrade(cfg, "20261112_0078")
|
||||
|
||||
with Session(mig_engine) as session:
|
||||
order = WeiboExternalOrder(
|
||||
order_no="MIG-0079-PENDING",
|
||||
status="exchange_pending",
|
||||
exchange_started_at=datetime(2026, 11, 12, 8, 30, tzinfo=UTC),
|
||||
with mig_engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO weibo_external_orders "
|
||||
"(order_no, status, exchange_started_at, created_at, updated_at) "
|
||||
"VALUES ('MIG-0079-PENDING', 'exchange_pending', "
|
||||
"'2026-11-12 08:30:00', "
|
||||
"'2026-11-12 08:00:00', '2026-11-12 08:30:00')"
|
||||
)
|
||||
)
|
||||
session.add(order)
|
||||
session.commit()
|
||||
|
||||
command.upgrade(cfg, "20261112_0079")
|
||||
|
||||
@@ -467,3 +468,56 @@ class TestMigrationSmoke:
|
||||
mig_engine.dispose()
|
||||
with admin_engine.begin() as conn:
|
||||
conn.execute(text(f"DROP DATABASE IF EXISTS `{mig_db}`;"))
|
||||
|
||||
def test_0080_backfills_scripted_at_for_done_orders(self):
|
||||
creds = get_mysql_credentials()
|
||||
mig_db = f"douyu_login_mig_{secrets.token_hex(4)}"
|
||||
mig_url = (
|
||||
f"mysql+pymysql://{quote_plus(creds['user'])}:"
|
||||
f"{quote_plus(creds['password'])}@{creds['host']}:{creds['port']}/"
|
||||
f"{mig_db}?charset=utf8mb4"
|
||||
)
|
||||
|
||||
admin_engine = get_test_engine()
|
||||
with admin_engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
f"CREATE DATABASE `{mig_db}` "
|
||||
"CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"
|
||||
)
|
||||
)
|
||||
|
||||
original_url = database_module.DATABASE_URL
|
||||
mig_engine = create_engine(mig_url)
|
||||
try:
|
||||
database_module.DATABASE_URL = mig_url
|
||||
cfg = _migration_config()
|
||||
cfg.set_main_option("sqlalchemy.url", mig_url)
|
||||
command.upgrade(cfg, "20261112_0079")
|
||||
with mig_engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO weibo_external_orders "
|
||||
"(order_no, status, exchange_started_at, done_at, "
|
||||
" created_at, updated_at) "
|
||||
"VALUES ('MIG-0080-DONE', 'done', "
|
||||
"'2026-11-12 08:00:00', '2026-11-12 08:10:00', "
|
||||
"'2026-11-12 07:00:00', '2026-11-12 08:10:00')"
|
||||
)
|
||||
)
|
||||
|
||||
command.upgrade(cfg, "20261112_0080")
|
||||
|
||||
with mig_engine.connect() as conn:
|
||||
row = conn.execute(
|
||||
text(
|
||||
"SELECT scripted_at FROM weibo_external_orders "
|
||||
"WHERE order_no = 'MIG-0080-DONE'"
|
||||
)
|
||||
).one()
|
||||
assert row.scripted_at is not None
|
||||
finally:
|
||||
database_module.DATABASE_URL = original_url
|
||||
mig_engine.dispose()
|
||||
with admin_engine.begin() as conn:
|
||||
conn.execute(text(f"DROP DATABASE IF EXISTS `{mig_db}`;"))
|
||||
|
||||
@@ -146,7 +146,9 @@ def test_exchange_record_unique_match_and_archive(monkeypatch, db):
|
||||
|
||||
monkeypatch.setattr(collab, "WeiboClient", FakeClient)
|
||||
collab.set_status(db, user, first, collab.STATUS_RECHARGED)
|
||||
collab.set_status(db, user, first, collab.STATUS_SCRIPTED)
|
||||
collab.set_status(db, user, second, collab.STATUS_RECHARGED)
|
||||
assert first.scripted_at is not None
|
||||
|
||||
result = collab.check_exchange_records(db, user, [first, second])
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""微博外部单协作增加上脚本时间,并回填已完成订单。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261112_0080"
|
||||
down_revision: str | None = "20261112_0079"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
TABLE = "weibo_external_orders"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
if not inspector.has_table(TABLE):
|
||||
return
|
||||
columns = {column["name"] for column in inspector.get_columns(TABLE)}
|
||||
if "scripted_at" not in columns:
|
||||
op.add_column(TABLE, sa.Column("scripted_at", sa.DateTime(), nullable=True))
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
UPDATE {TABLE}
|
||||
SET scripted_at = COALESCE(scripted_at, exchange_started_at, done_at, updated_at)
|
||||
WHERE status IN ('scripted', 'done')
|
||||
AND scripted_at IS NULL
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
if not inspector.has_table(TABLE):
|
||||
return
|
||||
columns = {column["name"] for column in inspector.get_columns(TABLE)}
|
||||
if "scripted_at" in columns:
|
||||
op.drop_column(TABLE, "scripted_at")
|
||||
@@ -1115,6 +1115,7 @@ class WeiboExternalOrder(Base):
|
||||
String(20), default="incomplete", server_default="incomplete", index=True
|
||||
)
|
||||
recharged_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
scripted_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
exchange_started_at: Mapped[datetime | None] = mapped_column(
|
||||
UTCDateTime, nullable=True
|
||||
)
|
||||
|
||||
@@ -934,6 +934,7 @@ class WeiboCollabOrderOut(BaseModel):
|
||||
exchange_verdict: str = ""
|
||||
exchange_message: str = ""
|
||||
recharged_at: datetime | None = None
|
||||
scripted_at: datetime | None = None
|
||||
done_at: datetime | None = None
|
||||
void_reason: str = ""
|
||||
is_archived: bool = False
|
||||
|
||||
@@ -33,6 +33,7 @@ from .weibo_service import query_weibo_mall, refresh_weibo_account
|
||||
STATUS_INCOMPLETE = "incomplete"
|
||||
STATUS_TO_RECHARGE = "to_recharge"
|
||||
STATUS_RECHARGED = "recharged"
|
||||
STATUS_SCRIPTED = "scripted"
|
||||
STATUS_DONE = "done"
|
||||
STATUS_WEIDOU_ERROR = "weidou_error"
|
||||
STATUS_FAILED = "failed"
|
||||
@@ -42,6 +43,7 @@ ORDER_STATUSES = (
|
||||
STATUS_INCOMPLETE,
|
||||
STATUS_TO_RECHARGE,
|
||||
STATUS_RECHARGED,
|
||||
STATUS_SCRIPTED,
|
||||
STATUS_DONE,
|
||||
STATUS_WEIDOU_ERROR,
|
||||
STATUS_FAILED,
|
||||
@@ -52,6 +54,7 @@ ORDER_STATUS_LABELS = {
|
||||
STATUS_INCOMPLETE: "待补资料",
|
||||
STATUS_TO_RECHARGE: "待充值",
|
||||
STATUS_RECHARGED: "已充值",
|
||||
STATUS_SCRIPTED: "已上脚本",
|
||||
STATUS_DONE: "兑换成功",
|
||||
STATUS_WEIDOU_ERROR: "微豆异常",
|
||||
STATUS_FAILED: "兑换失败",
|
||||
@@ -62,13 +65,15 @@ STATUS_FLOW = (
|
||||
STATUS_INCOMPLETE,
|
||||
STATUS_TO_RECHARGE,
|
||||
STATUS_RECHARGED,
|
||||
STATUS_SCRIPTED,
|
||||
STATUS_DONE,
|
||||
)
|
||||
STAGE_INDEX = {status: index for index, status in enumerate(STATUS_FLOW)}
|
||||
NEXT_STATUS = {
|
||||
STATUS_INCOMPLETE: STATUS_TO_RECHARGE,
|
||||
STATUS_TO_RECHARGE: STATUS_RECHARGED,
|
||||
STATUS_RECHARGED: STATUS_DONE,
|
||||
STATUS_RECHARGED: STATUS_SCRIPTED,
|
||||
STATUS_SCRIPTED: STATUS_DONE,
|
||||
}
|
||||
|
||||
BALANCE_GROUP_STATUSES = (
|
||||
@@ -712,15 +717,25 @@ def set_status(
|
||||
now = _now()
|
||||
if target == STATUS_TO_RECHARGE:
|
||||
order.recharged_at = None
|
||||
order.scripted_at = None
|
||||
order.exchange_started_at = None
|
||||
order.done_at = None
|
||||
elif target in (STATUS_RECHARGED, STATUS_WEIDOU_ERROR):
|
||||
if order.recharged_at is None:
|
||||
order.recharged_at = now
|
||||
if rollback:
|
||||
order.scripted_at = None
|
||||
order.exchange_started_at = None
|
||||
order.done_at = None
|
||||
elif target == STATUS_SCRIPTED:
|
||||
if order.recharged_at is None:
|
||||
raise CollabError("请先完成余额核账", code="recharge_check_required")
|
||||
if order.scripted_at is None:
|
||||
order.scripted_at = now
|
||||
order.done_at = None
|
||||
elif target == STATUS_DONE:
|
||||
if order.scripted_at is None:
|
||||
order.scripted_at = now
|
||||
if order.exchange_started_at is None:
|
||||
order.exchange_started_at = now
|
||||
order.done_at = now
|
||||
@@ -1216,7 +1231,7 @@ def check_exchange_records(
|
||||
order.exchange_record_snapshot = _json_safe(record)
|
||||
order.exchange_verdict = VERDICT_SUCCESS
|
||||
order.exchange_message = "兑换记录唯一命中"
|
||||
if order.status == STATUS_RECHARGED:
|
||||
if order.status in (STATUS_RECHARGED, STATUS_SCRIPTED):
|
||||
set_status(
|
||||
db,
|
||||
current,
|
||||
@@ -1518,6 +1533,7 @@ def serialize_order(
|
||||
"exchange_verdict": order.exchange_verdict or "",
|
||||
"exchange_message": order.exchange_message or "",
|
||||
"recharged_at": order.recharged_at,
|
||||
"scripted_at": order.scripted_at,
|
||||
"done_at": order.done_at,
|
||||
"void_reason": order.void_reason or "",
|
||||
"is_archived": order.archived_at is not None,
|
||||
|
||||
@@ -4,6 +4,7 @@ export type WeiboCollabStatus =
|
||||
| 'incomplete'
|
||||
| 'to_recharge'
|
||||
| 'recharged'
|
||||
| 'scripted'
|
||||
| 'done'
|
||||
| 'weidou_error'
|
||||
| 'failed'
|
||||
@@ -119,6 +120,7 @@ export interface WeiboCollabOrder {
|
||||
exchange_verdict: WeiboCollabVerdict;
|
||||
exchange_message: string;
|
||||
recharged_at: string | null;
|
||||
scripted_at: string | null;
|
||||
done_at: string | null;
|
||||
void_reason: string;
|
||||
is_archived: boolean;
|
||||
|
||||
@@ -72,6 +72,7 @@ const STATUS_META: Record<WeiboCollabStatus, { label: string; color: string }> =
|
||||
incomplete: { label: '待补资料', color: 'default' },
|
||||
to_recharge: { label: '待充值', color: 'blue' },
|
||||
recharged: { label: '已充值', color: 'cyan' },
|
||||
scripted: { label: '已上脚本', color: 'purple' },
|
||||
done: { label: '兑换成功', color: 'success' },
|
||||
weidou_error: { label: '微豆异常', color: 'orange' },
|
||||
failed: { label: '兑换失败', color: 'error' },
|
||||
@@ -84,10 +85,17 @@ const ACCOUNT_CK_LABELS: Record<string, string> = {
|
||||
error: '查询失败',
|
||||
};
|
||||
|
||||
const ACCOUNT_CK_COLORS: Record<string, string> = {
|
||||
valid: 'success',
|
||||
invalid: 'error',
|
||||
error: 'warning',
|
||||
};
|
||||
|
||||
const STATUS_ORDER: WeiboCollabStatus[] = [
|
||||
'incomplete',
|
||||
'to_recharge',
|
||||
'recharged',
|
||||
'scripted',
|
||||
'done',
|
||||
'weidou_error',
|
||||
'failed',
|
||||
@@ -115,6 +123,117 @@ const VERDICT_META: Record<string, { label: string; color: string }> = {
|
||||
'': { label: '未判定', color: 'default' },
|
||||
};
|
||||
|
||||
const TIMELINE_ACTION_LABELS: Record<string, string> = {
|
||||
'weibo:collab:create': '新建协作单',
|
||||
'weibo:collab:update': '编辑资料',
|
||||
'weibo:collab:verify': '校验账号',
|
||||
'weibo:collab:status': '修改状态',
|
||||
'weibo:collab:status_auto': '自动流转',
|
||||
'weibo:collab:balance_check': '充值核账',
|
||||
'weibo:collab:exchange_check': '查询兑换记录',
|
||||
'weibo:collab:void': '作废订单',
|
||||
'weibo:collab:archive': '归档订单',
|
||||
'weibo:collab:unarchive': '取消归档',
|
||||
};
|
||||
|
||||
const TIMELINE_FIELD_LABELS: Record<string, string> = {
|
||||
from: '原状态',
|
||||
to: '新状态',
|
||||
reason: '原因',
|
||||
account_id: '账号 ID',
|
||||
uid: 'UID',
|
||||
role: '绑定角色',
|
||||
ck_status: 'CK 状态',
|
||||
balance: '余额',
|
||||
required_total: '需求合计',
|
||||
surplus: '结余',
|
||||
threshold: '阈值',
|
||||
goods_name: '商品',
|
||||
required_weidou: '所需微豆',
|
||||
shop: '店铺',
|
||||
draft: '草稿',
|
||||
order_no: '订单号',
|
||||
records_total: '兑换记录数',
|
||||
records_matched: '匹配数',
|
||||
verdict: '兑换结论',
|
||||
message: '说明',
|
||||
};
|
||||
|
||||
function statusLabel(value: unknown): string {
|
||||
const key = String(value || '');
|
||||
return STATUS_META[key as WeiboCollabStatus]?.label || key || '-';
|
||||
}
|
||||
|
||||
function metricSourceLabel(source: string): string {
|
||||
if (source === 'balance_check') return '充值核账';
|
||||
if (source === 'exchange_records') return '兑换记录';
|
||||
return source || '其他查询';
|
||||
}
|
||||
|
||||
function timelineActionLabel(action: string): string {
|
||||
return TIMELINE_ACTION_LABELS[action] || action || '其他操作';
|
||||
}
|
||||
|
||||
function timelineDetailText(action: string, detail: string): string {
|
||||
let data: Record<string, unknown>;
|
||||
try {
|
||||
const parsed = JSON.parse(detail) as unknown;
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return detail || '-';
|
||||
}
|
||||
data = parsed as Record<string, unknown>;
|
||||
} catch {
|
||||
return detail || '-';
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
if (data.from !== undefined || data.to !== undefined) {
|
||||
parts.push(`${statusLabel(data.from)} → ${statusLabel(data.to)}`);
|
||||
}
|
||||
if (action === 'weibo:collab:balance_check') {
|
||||
parts.push(
|
||||
`余额 ${Number(data.balance ?? 0).toLocaleString()} / 需求 ${Number(data.required_total ?? 0).toLocaleString()} / 结余 ${Number(data.surplus ?? 0).toLocaleString()}`,
|
||||
);
|
||||
}
|
||||
if (action === 'weibo:collab:exchange_check') {
|
||||
parts.push(
|
||||
`匹配 ${data.records_matched ?? 0}/${data.records_total ?? 0} 条兑换记录`,
|
||||
);
|
||||
}
|
||||
const skipKeys = new Set([
|
||||
'from',
|
||||
'to',
|
||||
'balance',
|
||||
'required_total',
|
||||
'surplus',
|
||||
'records_matched',
|
||||
'records_total',
|
||||
'account_id',
|
||||
'order_ids',
|
||||
'group_key',
|
||||
]);
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (skipKeys.has(key) || value === null || value === undefined || value === '') {
|
||||
return;
|
||||
}
|
||||
const label = TIMELINE_FIELD_LABELS[key] || '详情';
|
||||
const rendered =
|
||||
typeof value === 'boolean'
|
||||
? value
|
||||
? '是'
|
||||
: '否'
|
||||
: typeof value === 'object'
|
||||
? value &&
|
||||
'old' in value &&
|
||||
'new' in value
|
||||
? `${String((value as { old?: unknown }).old ?? '-')} → ${String((value as { new?: unknown }).new ?? '-')}`
|
||||
: '已更新'
|
||||
: String(value);
|
||||
parts.push(`${label}:${rendered}`);
|
||||
});
|
||||
return parts.join(';') || '无附加信息';
|
||||
}
|
||||
|
||||
function balanceMeta(reason: string) {
|
||||
if (reason === 'ok') return { label: '余额正常', color: 'success' };
|
||||
if (reason === 'shortage') return { label: '微豆不足', color: 'error' };
|
||||
@@ -813,6 +932,36 @@ export default function WeiboCollabPage() {
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'CK 状态',
|
||||
dataIndex: 'account_ck_status',
|
||||
width: 140,
|
||||
render: (value: string, row) => {
|
||||
const label = ACCOUNT_CK_LABELS[value] || value || '未检测';
|
||||
return (
|
||||
<Space orientation="vertical" size={2} style={{ width: '100%' }}>
|
||||
<Tag color={ACCOUNT_CK_COLORS[value] || 'default'}>{label}</Tag>
|
||||
{row.account_ck_message && (
|
||||
<Tooltip title={row.account_ck_message}>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
display: 'block',
|
||||
maxWidth: 120,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{row.account_ck_message}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品 / 微豆',
|
||||
width: 190,
|
||||
@@ -1225,7 +1374,7 @@ export default function WeiboCollabPage() {
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={orders}
|
||||
scroll={{ x: 1500 }}
|
||||
scroll={{ x: 1650 }}
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedIds,
|
||||
onChange: (keys) => setSelectedIds(keys.map(Number)),
|
||||
@@ -1439,19 +1588,99 @@ export default function WeiboCollabPage() {
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
title={detail ? `协作单详情 · ${detail.order_no}` : '协作单详情'}
|
||||
title={
|
||||
detail ? (
|
||||
<Space size={10} wrap>
|
||||
<span>协作单详情</span>
|
||||
<Text code copyable={{ text: detail.order_no }}>
|
||||
{detail.order_no}
|
||||
</Text>
|
||||
<Tag color={STATUS_META[detail.status]?.color}>
|
||||
{detail.status_label}
|
||||
</Tag>
|
||||
</Space>
|
||||
) : (
|
||||
'协作单详情'
|
||||
)
|
||||
}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
size={880}
|
||||
size="min(1180px, 96vw)"
|
||||
loading={detailLoading}
|
||||
>
|
||||
{detail && (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Row gutter={[12, 12]}>
|
||||
{[
|
||||
{
|
||||
title: '账号余额',
|
||||
value: detail.weidou_balance,
|
||||
suffix: '微豆',
|
||||
color: undefined,
|
||||
},
|
||||
{
|
||||
title: '礼包需求',
|
||||
value: detail.weidou_required_total ?? detail.required_weidou,
|
||||
suffix: '微豆',
|
||||
color: undefined,
|
||||
},
|
||||
{
|
||||
title: '当前结余',
|
||||
value: detail.weidou_surplus,
|
||||
suffix: '微豆',
|
||||
color:
|
||||
detail.weidou_surplus != null &&
|
||||
(detail.weidou_surplus < 0 ||
|
||||
(detail.weidou_threshold != null &&
|
||||
detail.weidou_surplus > detail.weidou_threshold))
|
||||
? token.colorError
|
||||
: '#389e0d',
|
||||
},
|
||||
{
|
||||
title: '允许结余',
|
||||
value: detail.weidou_threshold,
|
||||
suffix: '微豆',
|
||||
color: undefined,
|
||||
},
|
||||
].map((item) => (
|
||||
<Col xs={12} md={6} key={item.title}>
|
||||
<div
|
||||
style={{
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRadius: 6,
|
||||
background: token.colorBgContainer,
|
||||
padding: '10px 12px',
|
||||
}}
|
||||
>
|
||||
<Statistic
|
||||
title={item.title}
|
||||
value={item.value ?? '-'}
|
||||
suffix={item.value == null ? undefined : item.suffix}
|
||||
valueStyle={{
|
||||
fontSize: 22,
|
||||
color: item.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<Descriptions
|
||||
size="small"
|
||||
column={2}
|
||||
bordered
|
||||
column={3}
|
||||
items={[
|
||||
{ key: 'shop', label: '店铺 / 负责人', children: `${detail.shop || '-'} / ${detail.owner_name || '-'}` },
|
||||
{ key: 'shop', label: '店铺', children: detail.shop || '-' },
|
||||
{ key: 'owner', label: '负责人', children: detail.owner_name || '-' },
|
||||
{
|
||||
key: 'status',
|
||||
label: '业务状态',
|
||||
children: (
|
||||
<Tag color={STATUS_META[detail.status]?.color}>
|
||||
{detail.status_label}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
label: '微博账号',
|
||||
@@ -1487,21 +1716,84 @@ export default function WeiboCollabPage() {
|
||||
),
|
||||
},
|
||||
{ key: 'role', label: '绑定角色', children: detail.game_role_name || '-' },
|
||||
{ key: 'goods', label: '商品 / 所需微豆', children: `${detail.goods_name || '-'} / ${detail.required_weidou ?? '-'}` },
|
||||
{ key: 'status', label: '业务状态', children: <Tag color={STATUS_META[detail.status]?.color}>{detail.status_label}</Tag> },
|
||||
{ key: 'balance', label: '最近核账', children: detail.weidou_check_message || '-' },
|
||||
{ key: 'exchange', label: '兑换确认', children: detail.exchange_message || '-' },
|
||||
{ key: 'updated', label: '最后修改', children: `${detail.updated_by_name || '-'} · ${formatTime(detail.updated_at)}` },
|
||||
{
|
||||
key: 'ck',
|
||||
label: 'CK 状态',
|
||||
children:
|
||||
ACCOUNT_CK_LABELS[detail.account_ck_status] ||
|
||||
detail.account_ck_status ||
|
||||
'未检测',
|
||||
},
|
||||
{
|
||||
key: 'goods',
|
||||
label: '商品',
|
||||
children: detail.goods_name || '-',
|
||||
},
|
||||
{
|
||||
key: 'required',
|
||||
label: '所需微豆',
|
||||
children:
|
||||
detail.required_weidou?.toLocaleString() ?? '-',
|
||||
},
|
||||
{
|
||||
key: 'checked',
|
||||
label: '最近核账',
|
||||
children: formatTime(detail.weidou_checked_at),
|
||||
},
|
||||
{
|
||||
key: 'scripted',
|
||||
label: '上脚本时间',
|
||||
children: formatTime(detail.scripted_at),
|
||||
},
|
||||
{
|
||||
key: 'balance_message',
|
||||
label: '核账结论',
|
||||
span: 2,
|
||||
children: detail.weidou_check_message || '尚未核账',
|
||||
},
|
||||
{
|
||||
key: 'exchange',
|
||||
label: '兑换确认',
|
||||
span: 3,
|
||||
children: detail.exchange_message || '尚未查询兑换记录',
|
||||
},
|
||||
{
|
||||
key: 'created',
|
||||
label: '创建记录',
|
||||
children: `${detail.created_by_name || '-'} · ${formatTime(detail.created_at)}`,
|
||||
},
|
||||
{
|
||||
key: 'updated',
|
||||
label: '最后修改',
|
||||
span: 2,
|
||||
children: `${detail.updated_by_name || '-'} · ${formatTime(detail.updated_at)}`,
|
||||
},
|
||||
...(detail.remark
|
||||
? [
|
||||
{
|
||||
key: 'remark',
|
||||
label: '备注',
|
||||
span: 3,
|
||||
children: detail.remark,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
{detail.home_images.length > 0 && (
|
||||
<Space wrap>
|
||||
<Space wrap size={10}>
|
||||
{detail.home_images.map((key) => (
|
||||
<img
|
||||
key={key}
|
||||
src={`/api/files/object?key=${encodeURIComponent(key)}&variant=thumb`}
|
||||
alt="主页截图"
|
||||
style={{ width: 96, height: 96, objectFit: 'cover', borderRadius: 4 }}
|
||||
style={{
|
||||
width: 112,
|
||||
height: 112,
|
||||
objectFit: 'cover',
|
||||
borderRadius: 6,
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
@@ -1526,23 +1818,70 @@ export default function WeiboCollabPage() {
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ x: 980 }}
|
||||
locale={{ emptyText: '暂无核账或兑换记录' }}
|
||||
dataSource={detailMetrics}
|
||||
columns={[
|
||||
{ title: '时间', dataIndex: 'created_at', width: 170, render: formatTime },
|
||||
{ title: '来源', dataIndex: 'source', width: 130 },
|
||||
{
|
||||
title: '核账',
|
||||
width: 220,
|
||||
render: (_, row) =>
|
||||
`余额 ${row.weidou_balance ?? '-'} / 合计 ${row.weidou_required_total ?? '-'} / 结余 ${row.weidou_surplus ?? '-'}`,
|
||||
title: '查询时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: formatTime,
|
||||
},
|
||||
{
|
||||
title: '兑换',
|
||||
width: 220,
|
||||
render: (_, row) =>
|
||||
`${row.verdict_label || '未判定'} · ${row.records_matched}/${row.records_total}`,
|
||||
title: '查询类型',
|
||||
dataIndex: 'source',
|
||||
width: 110,
|
||||
render: (value: string) => metricSourceLabel(value),
|
||||
},
|
||||
{
|
||||
title: '账号余额',
|
||||
dataIndex: 'weidou_balance',
|
||||
width: 110,
|
||||
align: 'right',
|
||||
render: (value: number | null) =>
|
||||
value?.toLocaleString() ?? '-',
|
||||
},
|
||||
{
|
||||
title: '需求合计',
|
||||
dataIndex: 'weidou_required_total',
|
||||
width: 110,
|
||||
align: 'right',
|
||||
render: (value: number | null) =>
|
||||
value?.toLocaleString() ?? '-',
|
||||
},
|
||||
{
|
||||
title: '结余',
|
||||
dataIndex: 'weidou_surplus',
|
||||
width: 110,
|
||||
align: 'right',
|
||||
render: (value: number | null) => (
|
||||
<Text type={value != null && value < 0 ? 'danger' : undefined}>
|
||||
{value?.toLocaleString() ?? '-'}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '兑换匹配',
|
||||
width: 120,
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Tag color={VERDICT_META[row.verdict]?.color}>
|
||||
{row.verdict_label ||
|
||||
VERDICT_META[row.verdict]?.label ||
|
||||
'未判定'}
|
||||
</Tag>
|
||||
<Text type="secondary">
|
||||
{row.records_matched}/{row.records_total}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结论',
|
||||
dataIndex: 'message',
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{ title: '结论', dataIndex: 'message' },
|
||||
]}
|
||||
/>
|
||||
<Title level={5}>操作时间线</Title>
|
||||
@@ -1550,12 +1889,38 @@ export default function WeiboCollabPage() {
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
scroll={{ x: 980 }}
|
||||
locale={{ emptyText: '暂无操作记录' }}
|
||||
dataSource={detailTimeline}
|
||||
columns={[
|
||||
{ title: '时间', dataIndex: 'created_at', width: 170, render: formatTime },
|
||||
{ title: '操作人', dataIndex: 'username', width: 120 },
|
||||
{ title: '动作', dataIndex: 'action', width: 190 },
|
||||
{ title: '详情', dataIndex: 'detail' },
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: formatTime,
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'username',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '动作',
|
||||
dataIndex: 'action',
|
||||
width: 140,
|
||||
render: (value: string) => (
|
||||
<Tag color="blue">{timelineActionLabel(value)}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '详情',
|
||||
dataIndex: 'detail',
|
||||
render: (value: string, row) => (
|
||||
<Text style={{ fontSize: 13 }}>
|
||||
{timelineDetailText(row.action, value)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
Reference in New Issue
Block a user