feat: 重做协作台账状态机并支持充值核账
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""虎牙协作台账:待充值 / 积分异常 + 礼包积分快照。
|
||||
|
||||
2026-10-19 用户拍板:
|
||||
- 删除 pending(待处理)/ bound(已确认绑定);
|
||||
- 资料齐全且 CK 有效自动进 to_recharge(待充值);
|
||||
- 充值完成按同一 bind_game_account 整组核账,结余为负或超过阈值进
|
||||
points_error(积分异常)。
|
||||
|
||||
同时新增 required_points 与最近一次核账快照字段。可重复执行:逐列检查。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20261019_0061"
|
||||
down_revision: str | None = "20261019_0060"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
TABLE = "huya_external_orders"
|
||||
|
||||
|
||||
def _columns(bind) -> set[str]:
|
||||
return {column["name"] for column in sa.inspect(bind).get_columns(TABLE)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if TABLE not in set(sa.inspect(bind).get_table_names()):
|
||||
return
|
||||
columns = _columns(bind)
|
||||
|
||||
additions = {
|
||||
"required_points": sa.Column("required_points", sa.Integer(), nullable=True),
|
||||
"points_checked_at": sa.Column(
|
||||
"points_checked_at", sa.DateTime(), nullable=True
|
||||
),
|
||||
"points_balance": sa.Column("points_balance", sa.Integer(), nullable=True),
|
||||
"points_required_total": sa.Column(
|
||||
"points_required_total", sa.Integer(), nullable=True
|
||||
),
|
||||
"points_surplus": sa.Column("points_surplus", sa.Integer(), nullable=True),
|
||||
"points_threshold": sa.Column("points_threshold", sa.Integer(), nullable=True),
|
||||
"points_check_reason": sa.Column(
|
||||
"points_check_reason",
|
||||
sa.String(length=16),
|
||||
nullable=False,
|
||||
server_default="",
|
||||
),
|
||||
"points_check_message": sa.Column(
|
||||
"points_check_message",
|
||||
sa.String(length=512),
|
||||
nullable=False,
|
||||
server_default="",
|
||||
),
|
||||
"points_check_detail": sa.Column(
|
||||
"points_check_detail", sa.JSON(), nullable=True
|
||||
),
|
||||
}
|
||||
for name, column in additions.items():
|
||||
if name not in columns:
|
||||
op.add_column(TABLE, column)
|
||||
|
||||
op.alter_column(
|
||||
TABLE,
|
||||
"status",
|
||||
existing_type=sa.String(length=16),
|
||||
existing_nullable=False,
|
||||
server_default="incomplete",
|
||||
)
|
||||
|
||||
# 旧 pending/bound:资料齐全的转待充值,其余退回待补资料。
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
UPDATE {TABLE}
|
||||
SET status = 'to_recharge'
|
||||
WHERE status IN ('pending', 'bound')
|
||||
AND cookie IS NOT NULL AND cookie <> ''
|
||||
AND ck_status = 'valid'
|
||||
AND bind_game_account IS NOT NULL AND bind_game_account <> ''
|
||||
AND bind_game_role IS NOT NULL AND bind_game_role <> ''
|
||||
AND gift_name IS NOT NULL AND gift_name <> ''
|
||||
AND home_images IS NOT NULL
|
||||
AND JSON_LENGTH(home_images) > 0
|
||||
"""
|
||||
)
|
||||
)
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"""
|
||||
UPDATE {TABLE}
|
||||
SET status = 'incomplete'
|
||||
WHERE status IN ('pending', 'bound')
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if TABLE not in set(sa.inspect(bind).get_table_names()):
|
||||
return
|
||||
# 新状态尽量映射回旧口径;历史 pending 不恢复。
|
||||
bind.execute(
|
||||
sa.text(f"UPDATE {TABLE} SET status = 'bound' WHERE status = 'to_recharge'")
|
||||
)
|
||||
bind.execute(
|
||||
sa.text(
|
||||
f"UPDATE {TABLE} SET status = 'recharged' WHERE status = 'points_error'"
|
||||
)
|
||||
)
|
||||
op.alter_column(
|
||||
TABLE,
|
||||
"status",
|
||||
existing_type=sa.String(length=16),
|
||||
existing_nullable=False,
|
||||
server_default="pending",
|
||||
)
|
||||
columns = _columns(bind)
|
||||
for column in (
|
||||
"points_check_detail",
|
||||
"points_check_message",
|
||||
"points_check_reason",
|
||||
"points_threshold",
|
||||
"points_surplus",
|
||||
"points_required_total",
|
||||
"points_balance",
|
||||
"points_checked_at",
|
||||
"required_points",
|
||||
):
|
||||
if column in columns:
|
||||
op.drop_column(TABLE, column)
|
||||
+21
-1
@@ -1157,11 +1157,14 @@ class HuyaExternalOrder(Base):
|
||||
|
||||
# ---- Excel G 列(表头写的「备注」,实际是礼包名)+ 自由备注 ----
|
||||
gift_name: Mapped[str] = mapped_column(String(128), default="", server_default="")
|
||||
# 购买该礼包所需积分快照(来自积分中心 queryAllocation 的 tag75.tag0.tag2)。
|
||||
# 积分会随活动/商品变化,必须在下单时存一份,不能每次按礼包名现查。
|
||||
required_points: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
remark: Mapped[str] = mapped_column(String(512), default="", server_default="")
|
||||
|
||||
# ---- 业务进度 ----
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(16), default="pending", server_default="pending", index=True
|
||||
String(16), default="incomplete", server_default="incomplete", index=True
|
||||
)
|
||||
bound_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
recharged_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
@@ -1169,6 +1172,23 @@ class HuyaExternalOrder(Base):
|
||||
done_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
void_reason: Mapped[str] = mapped_column(String(256), default="", server_default="")
|
||||
|
||||
# ---- 充值核账快照(按 bind_game_account 整组计算)----
|
||||
points_checked_at: Mapped[datetime | None] = mapped_column(
|
||||
UTCDateTime, nullable=True
|
||||
)
|
||||
points_balance: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
points_required_total: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
points_surplus: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
points_threshold: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
points_check_reason: Mapped[str] = mapped_column(
|
||||
String(16), default="", server_default=""
|
||||
)
|
||||
points_check_message: Mapped[str] = mapped_column(
|
||||
String(512), default="", server_default=""
|
||||
)
|
||||
# 组内订单、各 CK 余额、缺口/多余等明细(JSON,只做展示与审计)
|
||||
points_check_detail: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
|
||||
# ---- 归档(与业务状态正交:归档后仍是「抢购成功」,只是默认不出现在主台账)----
|
||||
# NULL = 未归档;软隐藏不删数据,取消归档即可恢复。
|
||||
archived_at: Mapped[datetime | None] = mapped_column(
|
||||
|
||||
@@ -57,6 +57,8 @@ from ...schemas import (
|
||||
HuyaExternalOrderListOut,
|
||||
HuyaExternalOrderOut,
|
||||
HuyaExternalOrderOwnerOut,
|
||||
HuyaExternalOrderRechargeOut,
|
||||
HuyaExternalOrderRechargeRequest,
|
||||
HuyaExternalOrderStatusBatchOut,
|
||||
HuyaExternalOrderStatusBatchRequest,
|
||||
HuyaExternalOrderStatusRequest,
|
||||
@@ -317,6 +319,7 @@ def create_external_order(
|
||||
ck_text=req.ck_text,
|
||||
bind_douyu_account=req.bind_douyu_account,
|
||||
gift_name=req.gift_name,
|
||||
required_points=req.required_points,
|
||||
home_images=req.home_images,
|
||||
remark=req.remark,
|
||||
require_verified=req.require_verified,
|
||||
@@ -374,6 +377,27 @@ def preview_external_order_ck(
|
||||
return preview
|
||||
|
||||
|
||||
@router.post("/external-orders/recharge", response_model=HuyaExternalOrderRechargeOut)
|
||||
def recharge_external_orders(
|
||||
req: HuyaExternalOrderRechargeRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("huya:collab:edit")),
|
||||
):
|
||||
"""充值核账:按绑定账号整组校验 CK + 积分,整组落「已充值 / 积分异常」。"""
|
||||
try:
|
||||
orders = collab.list_orders_by_ids(db, req.ids)
|
||||
result = collab.check_recharge_groups(
|
||||
db,
|
||||
current,
|
||||
orders,
|
||||
threshold=req.threshold,
|
||||
)
|
||||
except collab.CollabError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
_publish_change(current, scope="orders", action="orders.recharge")
|
||||
return result
|
||||
|
||||
|
||||
@router.post(
|
||||
"/external-orders/status/batch", response_model=HuyaExternalOrderStatusBatchOut
|
||||
)
|
||||
@@ -384,8 +408,8 @@ def batch_update_external_order_status(
|
||||
):
|
||||
"""勾选多行一次改业务状态(单个也走这里)。
|
||||
|
||||
人工切换允许**往前跳步**(待处理 → 已上脚本),回退要带 ``rollback=true``;
|
||||
某一行前置校验没过(如没主页截图不能进「已确认绑定」)只记在这一条的 ``message`` 里。
|
||||
人工切换允许**往前跳步**(待补资料 → 已上脚本),回退要带 ``rollback=true``;
|
||||
「已充值 / 积分异常」不走这里,必须用 ``POST /external-orders/recharge`` 整组核账。
|
||||
"""
|
||||
try:
|
||||
orders = collab.list_orders_by_ids(db, req.ids)
|
||||
|
||||
+56
-2
@@ -1141,7 +1141,7 @@ class HuyaCollabBuyerOrderOut(BaseModel):
|
||||
shop: str = ""
|
||||
gift_name: str = ""
|
||||
uid: str = ""
|
||||
status: str = "pending"
|
||||
status: str = "incomplete"
|
||||
status_label: str = ""
|
||||
created_at: datetime | None = None
|
||||
|
||||
@@ -1183,6 +1183,7 @@ class HuyaExternalOrderCreate(BaseModel):
|
||||
ck_text: str = ""
|
||||
bind_douyu_account: str = Field("", max_length=128)
|
||||
gift_name: str = Field("", max_length=128)
|
||||
required_points: int | None = Field(None, ge=0)
|
||||
# 主页截图(object_key 数组,第一张作封面)。绑定图不再收:绑定关系由探针自动读,
|
||||
# 人工截图没有意义(2026-10-16 用户拍板)。
|
||||
home_images: list[str] = Field(default_factory=list)
|
||||
@@ -1198,6 +1199,7 @@ class HuyaExternalOrderUpdate(BaseModel):
|
||||
bind_douyu_account: str | None = Field(None, max_length=128)
|
||||
bind_douyu_uid: str | None = Field(None, max_length=32)
|
||||
gift_name: str | None = Field(None, max_length=128)
|
||||
required_points: int | None = Field(None, ge=0)
|
||||
remark: str | None = Field(None, max_length=512)
|
||||
# 乐观锁:客户端拿到的版本号。不一致说明别人先改了 → 409。
|
||||
expected_version: int | None = None
|
||||
@@ -1325,6 +1327,49 @@ class HuyaExternalOrderStatusBatchOut(BaseModel):
|
||||
results: list[HuyaExternalOrderStatusBatchItemOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HuyaExternalOrderRechargeRequest(BaseModel):
|
||||
"""充值核账请求:按绑定账号整组核账,阈值默认 100。"""
|
||||
|
||||
ids: list[int] = Field(default_factory=list)
|
||||
threshold: int = Field(100, ge=0, le=1_000_000)
|
||||
|
||||
|
||||
class HuyaExternalOrderRechargeOrderOut(BaseModel):
|
||||
id: int
|
||||
order_no: str = ""
|
||||
status: str = ""
|
||||
status_label: str = ""
|
||||
|
||||
|
||||
class HuyaExternalOrderRechargeGroupOut(BaseModel):
|
||||
group_key: str = ""
|
||||
status: str = ""
|
||||
status_label: str = ""
|
||||
balance: int | None = None
|
||||
required_total: int | None = None
|
||||
surplus: int | None = None
|
||||
threshold: int = 100
|
||||
reason: str = ""
|
||||
message: str = ""
|
||||
order_ids: list[int] = Field(default_factory=list)
|
||||
orders: list[HuyaExternalOrderRechargeOrderOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HuyaExternalOrderRechargeFailureOut(BaseModel):
|
||||
id: int
|
||||
order_no: str = ""
|
||||
message: str = ""
|
||||
|
||||
|
||||
class HuyaExternalOrderRechargeOut(BaseModel):
|
||||
total: int
|
||||
updated: int
|
||||
failed: int
|
||||
threshold: int = 100
|
||||
groups: list[HuyaExternalOrderRechargeGroupOut] = Field(default_factory=list)
|
||||
failures: list[HuyaExternalOrderRechargeFailureOut] = Field(default_factory=list)
|
||||
|
||||
|
||||
class HuyaExternalOrderArchiveItemOut(BaseModel):
|
||||
"""归档/取消归档里的一行结论。"""
|
||||
|
||||
@@ -1534,15 +1579,24 @@ class HuyaExternalOrderOut(BaseModel):
|
||||
home_image_thumb_urls: list[str] = Field(default_factory=list)
|
||||
|
||||
gift_name: str = ""
|
||||
required_points: int | None = None
|
||||
remark: str = ""
|
||||
|
||||
status: str = "pending"
|
||||
status: str = "incomplete"
|
||||
status_label: str = ""
|
||||
next_status: str | None = None
|
||||
bound_at: datetime | None = None
|
||||
recharged_at: datetime | None = None
|
||||
scripted_at: datetime | None = None
|
||||
done_at: datetime | None = None
|
||||
points_checked_at: datetime | None = None
|
||||
points_balance: int | None = None
|
||||
points_required_total: int | None = None
|
||||
points_surplus: int | None = None
|
||||
points_threshold: int | None = None
|
||||
points_check_reason: str = ""
|
||||
points_check_message: str = ""
|
||||
points_check_detail: dict[str, Any] | None = None
|
||||
void_reason: str = ""
|
||||
|
||||
# 归档是可见性状态,不影响上面的业务状态。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,8 @@ import type {
|
||||
HuyaCollabOrderQuery,
|
||||
HuyaCollabOrderUpdate,
|
||||
HuyaCollabOwner,
|
||||
HuyaCollabRechargeRequest,
|
||||
HuyaCollabRechargeResult,
|
||||
HuyaCollabShop,
|
||||
HuyaCollabShopCreate,
|
||||
HuyaCollabShopUpdate,
|
||||
@@ -284,6 +286,12 @@ export const huyaApi = {
|
||||
'/huya/external-orders/status/batch',
|
||||
data,
|
||||
),
|
||||
/** 充值核账:按绑定账号整组校验 CK + 积分,整组落「已充值 / 积分异常」 */
|
||||
rechargeCollabOrders: (data: HuyaCollabRechargeRequest) =>
|
||||
api.post<HuyaCollabRechargeResult, HuyaCollabRechargeResult>(
|
||||
'/huya/external-orders/recharge',
|
||||
data,
|
||||
),
|
||||
/** 归档抢购成功的订单(可带备注;archive_filtered 时按当前筛选批量归档) */
|
||||
archiveCollabOrders: (data: HuyaCollabArchiveRequest) =>
|
||||
api.post<HuyaCollabArchiveResult, HuyaCollabArchiveResult>(
|
||||
|
||||
@@ -1202,11 +1202,13 @@ export interface AccountExchangeRecords {
|
||||
export type HuyaCollabStatus =
|
||||
/** 只登记了订单号 + 店铺,CK / 礼包 / 主页截图等客户资料还没到 */
|
||||
| 'incomplete'
|
||||
| 'pending'
|
||||
| 'bound'
|
||||
/** 资料齐全且 CK 有效;等充值 */
|
||||
| 'to_recharge'
|
||||
| 'recharged'
|
||||
| 'scripted'
|
||||
| 'done'
|
||||
/** 充值完成但积分不足或超出阈值(可重新核账恢复) */
|
||||
| 'points_error'
|
||||
/** 旁路终态:抢购后查兑换记录没查到这一单的礼包(人工确认后可作废或回退重试) */
|
||||
| 'failed'
|
||||
| 'void';
|
||||
@@ -1252,6 +1254,8 @@ export interface HuyaCollabOrder {
|
||||
bind_image_thumb_urls: string[];
|
||||
home_image_thumb_urls: string[];
|
||||
gift_name: string;
|
||||
/** 购买该礼包所需积分快照(来自积分中心 queryAllocation) */
|
||||
required_points: number | null;
|
||||
remark: string;
|
||||
status: HuyaCollabStatus;
|
||||
status_label: string;
|
||||
@@ -1260,6 +1264,14 @@ export interface HuyaCollabOrder {
|
||||
recharged_at: string | null;
|
||||
scripted_at: string | null;
|
||||
done_at: string | null;
|
||||
points_checked_at: string | null;
|
||||
points_balance: number | null;
|
||||
points_required_total: number | null;
|
||||
points_surplus: number | null;
|
||||
points_threshold: number | null;
|
||||
points_check_reason: string;
|
||||
points_check_message: string;
|
||||
points_check_detail: Record<string, unknown> | null;
|
||||
void_reason: string;
|
||||
/** 归档是可见性状态,不改业务状态;归档行只读 */
|
||||
is_archived: boolean;
|
||||
@@ -1430,6 +1442,49 @@ export interface HuyaCollabStatusBatchResult {
|
||||
results: HuyaCollabStatusBatchItem[];
|
||||
}
|
||||
|
||||
/** 充值核账:按绑定账号整组核账 */
|
||||
export interface HuyaCollabRechargeRequest {
|
||||
ids: number[];
|
||||
/** 允许结余上限,默认 100 */
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export interface HuyaCollabRechargeOrder {
|
||||
id: number;
|
||||
order_no: string;
|
||||
status: HuyaCollabStatus;
|
||||
status_label: string;
|
||||
}
|
||||
|
||||
export interface HuyaCollabRechargeGroup {
|
||||
group_key: string;
|
||||
status: HuyaCollabStatus | '';
|
||||
status_label: string;
|
||||
balance: number | null;
|
||||
required_total: number | null;
|
||||
surplus: number | null;
|
||||
threshold: number;
|
||||
reason: string;
|
||||
message: string;
|
||||
order_ids: number[];
|
||||
orders: HuyaCollabRechargeOrder[];
|
||||
}
|
||||
|
||||
export interface HuyaCollabRechargeFailure {
|
||||
id: number;
|
||||
order_no: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface HuyaCollabRechargeResult {
|
||||
total: number;
|
||||
updated: number;
|
||||
failed: number;
|
||||
threshold: number;
|
||||
groups: HuyaCollabRechargeGroup[];
|
||||
failures: HuyaCollabRechargeFailure[];
|
||||
}
|
||||
|
||||
/** 归档请求:ids 为空且 archive_filtered=true 时按筛选范围归档 */
|
||||
export interface HuyaCollabArchiveRequest {
|
||||
ids?: number[];
|
||||
@@ -1540,6 +1595,7 @@ export interface HuyaCollabOrderCreate {
|
||||
ck_text?: string;
|
||||
bind_douyu_account?: string;
|
||||
gift_name?: string;
|
||||
required_points?: number | null;
|
||||
/** 主页截图(object_key 数组,第一张作封面);严格模式下必填至少 1 张 */
|
||||
home_images?: string[];
|
||||
remark?: string;
|
||||
@@ -1556,6 +1612,7 @@ export interface HuyaCollabOrderUpdate {
|
||||
bind_douyu_account?: string;
|
||||
bind_douyu_uid?: string;
|
||||
gift_name?: string;
|
||||
required_points?: number | null;
|
||||
remark?: string;
|
||||
expected_version?: number;
|
||||
}
|
||||
@@ -1627,7 +1684,7 @@ export interface FileUploadResult {
|
||||
size: number;
|
||||
}
|
||||
|
||||
/** 活动礼包(用一份有效 CK 现拉) */
|
||||
/** 积分中心商品(用一份有效 CK 现拉;PlayMallUI.queryAllocation) */
|
||||
export interface HuyaCollabGift {
|
||||
product_id: string;
|
||||
name: string;
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert, App, Badge, Button, Card, Checkbox, Col, Descriptions, Divider, Drawer, Dropdown,
|
||||
Empty, Form, Image, Input, Modal, Popconfirm, Popover, Progress, Radio, Row, Select, Space, Statistic,
|
||||
Empty, Form, Image, Input, InputNumber, Modal, Popconfirm, Popover, Progress, Radio, Row, Select, Space, Statistic,
|
||||
Switch, Table, Tag, Timeline, Tooltip, Typography, Upload,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import {
|
||||
CheckCircleOutlined, CloudUploadOutlined, CopyOutlined, DeleteOutlined, EditOutlined, FileAddOutlined,
|
||||
FileExcelOutlined, GiftOutlined, InboxOutlined, LinkOutlined, PlusOutlined, ReloadOutlined, SafetyCertificateOutlined,
|
||||
ShopOutlined, StopOutlined, SwapOutlined, SyncOutlined, UndoOutlined,
|
||||
AuditOutlined, CheckCircleOutlined, CloudUploadOutlined, CopyOutlined, DeleteOutlined, EditOutlined,
|
||||
FileAddOutlined, FileExcelOutlined, GiftOutlined, InboxOutlined, LinkOutlined, PlusOutlined, ReloadOutlined,
|
||||
SafetyCertificateOutlined, ShopOutlined, StopOutlined, SwapOutlined, SyncOutlined, UndoOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
fileApi,
|
||||
@@ -70,13 +70,14 @@ function readRealtimePaused(): boolean {
|
||||
const STATUS_META: Record<HuyaCollabStatus, { label: string; color: string }> = {
|
||||
// 只登记了订单号 + 店铺,客户资料(CK / 礼包 / 主页截图)到了再补
|
||||
incomplete: { label: '待补资料', color: 'gold' },
|
||||
pending: { label: '待处理', color: 'default' },
|
||||
// 人工核对过「探针读到的绑定角色」与「主页截图」一致
|
||||
bound: { label: '已确认绑定', color: 'blue' },
|
||||
// 资料齐全且 CK 有效;等充值
|
||||
to_recharge: { label: '待充值', color: 'orange' },
|
||||
recharged: { label: '已充值', color: 'geekblue' },
|
||||
scripted: { label: '已上脚本', color: 'purple' },
|
||||
// 抢购成功(枚举值仍是 done):从「已上脚本」查到兑换记录 → 自动进这一步
|
||||
done: { label: '抢购成功', color: 'success' },
|
||||
// 充值完成但积分不足/超出阈值
|
||||
points_error: { label: '积分异常', color: 'volcano' },
|
||||
// 旁路终态:抢购后查兑换记录没查到这一单的礼包(枚举值仍是 failed)
|
||||
failed: { label: '抢购失败', color: 'red' },
|
||||
void: { label: '已作废', color: 'error' },
|
||||
@@ -92,9 +93,8 @@ const VERDICT_META: Record<string, { label: string; color: string }> = {
|
||||
/** 「改状态」能选的目标(顺序 = 业务流程顺序,回退要显式勾选)
|
||||
* 「待补资料」只作为新建起点,不放进「改状态」:它只能通过「回退状态」进入。 */
|
||||
const STATUS_ORDER_OPTIONS: { value: HuyaCollabStatus; label: string }[] = [
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'bound', label: '已确认绑定' },
|
||||
{ value: 'recharged', label: '已充值' },
|
||||
{ value: 'to_recharge', label: '待充值' },
|
||||
{ value: 'recharged', label: '已充值(核账)' },
|
||||
{ value: 'scripted', label: '已上脚本' },
|
||||
{ value: 'done', label: '抢购成功' },
|
||||
{ value: 'failed', label: '抢购失败' },
|
||||
@@ -104,9 +104,8 @@ const STATUS_ORDER_OPTIONS: { value: HuyaCollabStatus; label: string }[] = [
|
||||
/** 「回退」能选的目标:流程链(含「待补资料」;要进「已作废 / 抢购失败」走「改状态」或「作废」入口) */
|
||||
const FLOW_STATUS_OPTIONS: { value: HuyaCollabStatus; label: string }[] = [
|
||||
{ value: 'incomplete', label: '待补资料' },
|
||||
{ value: 'pending', label: '待处理' },
|
||||
{ value: 'bound', label: '已确认绑定' },
|
||||
{ value: 'recharged', label: '已充值' },
|
||||
{ value: 'to_recharge', label: '待充值' },
|
||||
{ value: 'recharged', label: '已充值(核账)' },
|
||||
{ value: 'scripted', label: '已上脚本' },
|
||||
{ value: 'done', label: '抢购成功' },
|
||||
];
|
||||
@@ -163,8 +162,21 @@ const SLOT_META: Record<HuyaCollabImageSlot, { label: string; keysKey: 'bind_ima
|
||||
|
||||
const IMPORT_PREVIEW_ROWS = 20;
|
||||
// 「礼包列表」的本地缓存键:一个活动期间就那一批礼包,没必要每贴一份 CK 都去虎牙拉一次;
|
||||
// 要更新就点「强制刷新礼包」(活动换 SID 后必点一次)。
|
||||
const GIFTS_CACHE_KEY = 'huya.collab.gifts';
|
||||
// v2 = 2026-10-19 切到积分中心 queryAllocation 后换键,避免继续用旧 getActPrizeList 的缓存。
|
||||
const GIFTS_CACHE_KEY = 'huya.collab.gifts.v2';
|
||||
const RECHARGE_THRESHOLD_KEY = 'huya.collab.rechargeThreshold';
|
||||
const RECHARGE_THRESHOLD_DEFAULT = 100;
|
||||
|
||||
function readRechargeThreshold(): number {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(RECHARGE_THRESHOLD_KEY);
|
||||
if (raw === null || raw === '') return RECHARGE_THRESHOLD_DEFAULT;
|
||||
const value = Number(raw);
|
||||
return Number.isFinite(value) && value >= 0 ? value : RECHARGE_THRESHOLD_DEFAULT;
|
||||
} catch {
|
||||
return RECHARGE_THRESHOLD_DEFAULT;
|
||||
}
|
||||
}
|
||||
const MAX_IMAGES_PER_SLOT = 20;
|
||||
|
||||
/** 读礼包缓存(读不到 / 解析坏 / 空列表都当没缓存) */
|
||||
@@ -201,6 +213,7 @@ interface CreateDraft {
|
||||
shop: string;
|
||||
ck_text: string;
|
||||
gift_name: string;
|
||||
required_points: number | null;
|
||||
remark: string;
|
||||
home_images: string[];
|
||||
saved_at: number;
|
||||
@@ -229,6 +242,7 @@ function readCreateDraft(userId?: number | null): CreateDraft | null {
|
||||
shop: typeof parsed.shop === 'string' ? parsed.shop : '',
|
||||
ck_text: typeof parsed.ck_text === 'string' ? parsed.ck_text : '',
|
||||
gift_name: typeof parsed.gift_name === 'string' ? parsed.gift_name : '',
|
||||
required_points: typeof parsed.required_points === 'number' ? parsed.required_points : null,
|
||||
remark: typeof parsed.remark === 'string' ? parsed.remark : '',
|
||||
home_images: Array.isArray(parsed.home_images)
|
||||
? parsed.home_images.filter((item): item is string => typeof item === 'string')
|
||||
@@ -287,6 +301,7 @@ interface SupplementDraft {
|
||||
shop: string;
|
||||
ck_text: string;
|
||||
gift_name: string;
|
||||
required_points: number | null;
|
||||
remark: string;
|
||||
home_images: string[];
|
||||
saved_at: number;
|
||||
@@ -296,6 +311,7 @@ interface SupplementFormValues {
|
||||
shop: string;
|
||||
ck_text?: string;
|
||||
gift_name?: string;
|
||||
required_points?: number | null;
|
||||
remark?: string;
|
||||
home_images?: string[];
|
||||
}
|
||||
@@ -320,6 +336,7 @@ function readSupplementDraft(userId: number | null | undefined, orderId: number)
|
||||
shop: typeof parsed.shop === 'string' ? parsed.shop : '',
|
||||
ck_text: typeof parsed.ck_text === 'string' ? parsed.ck_text : '',
|
||||
gift_name: typeof parsed.gift_name === 'string' ? parsed.gift_name : '',
|
||||
required_points: typeof parsed.required_points === 'number' ? parsed.required_points : null,
|
||||
remark: typeof parsed.remark === 'string' ? parsed.remark : '',
|
||||
home_images: Array.isArray(parsed.home_images)
|
||||
? parsed.home_images.filter((item): item is string => typeof item === 'string')
|
||||
@@ -510,6 +527,8 @@ export default function HuyaCollabPage() {
|
||||
const [realtimePending, setRealtimePending] = useState(false);
|
||||
const [realtimePaused, setRealtimePaused] = useState(readRealtimePaused);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
// 点顶部统计卡筛选后滚到列表,避免用户还要自己往下找
|
||||
const listAnchorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [detail, setDetail] = useState<HuyaCollabOrder | null>(null);
|
||||
const [timeline, setTimeline] = useState<HuyaCollabAuditItem[]>([]);
|
||||
@@ -560,12 +579,16 @@ export default function HuyaCollabPage() {
|
||||
order: HuyaCollabOrder; mode: 'void' | 'rollback' | 'set'; target?: HuyaCollabStatus;
|
||||
} | null>(null);
|
||||
const [statusReason, setStatusReason] = useState('');
|
||||
const [rollbackTarget, setRollbackTarget] = useState<HuyaCollabStatus>('pending');
|
||||
const [rollbackTarget, setRollbackTarget] = useState<HuyaCollabStatus>('to_recharge');
|
||||
// 批量改状态(勾选多行一次切)
|
||||
const [batchStatusOpen, setBatchStatusOpen] = useState(false);
|
||||
const [batchStatusTarget, setBatchStatusTarget] = useState<HuyaCollabStatus>('bound');
|
||||
const [batchStatusTarget, setBatchStatusTarget] = useState<HuyaCollabStatus>('to_recharge');
|
||||
const [batchStatusReason, setBatchStatusReason] = useState('');
|
||||
const [batchStatusRollback, setBatchStatusRollback] = useState(false);
|
||||
// 充值核账弹窗:按 bind_game_account 整组核账,阈值默认 100
|
||||
const [rechargeOpen, setRechargeOpen] = useState(false);
|
||||
const [rechargeIds, setRechargeIds] = useState<number[]>([]);
|
||||
const [rechargeThreshold, setRechargeThreshold] = useState(readRechargeThreshold);
|
||||
// 归档弹窗:ids=null 表示按当前筛选结果批量归档
|
||||
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||
const [archiveIds, setArchiveIds] = useState<number[] | null>(null);
|
||||
@@ -634,7 +657,7 @@ export default function HuyaCollabPage() {
|
||||
// 暂停后连 WebSocket 也不建:既不自动重拉,也不做断线兜底轮询。
|
||||
const realtimeSyncEnabled = realtimeEnabled && !realtimePaused;
|
||||
const collabModalOpen = Boolean(
|
||||
createOpen || supplementTarget || detail || statusAction || batchStatusOpen || archiveOpen || ckTarget || ckExportOpen || shopModalOpen,
|
||||
createOpen || supplementTarget || detail || statusAction || batchStatusOpen || rechargeOpen || archiveOpen || ckTarget || ckExportOpen || shopModalOpen,
|
||||
);
|
||||
|
||||
const toggleRealtimeSync = (nextEnabled: boolean) => {
|
||||
@@ -905,6 +928,11 @@ export default function HuyaCollabPage() {
|
||||
|
||||
/** 勾选多行一次改状态(往前可跳步;回退要在弹窗里勾「回退」) */
|
||||
const submitBatchStatus = async () => {
|
||||
if (batchStatusTarget === 'recharged') {
|
||||
setBatchStatusOpen(false);
|
||||
openRecharge(selectedIds);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await huyaApi.batchUpdateCollabOrderStatus({
|
||||
@@ -1095,6 +1123,7 @@ export default function HuyaCollabPage() {
|
||||
shop: row.shop,
|
||||
owner_id: row.owner_id,
|
||||
gift_name: row.gift_name,
|
||||
required_points: row.required_points,
|
||||
remark: row.remark,
|
||||
});
|
||||
setTimeline([]);
|
||||
@@ -1172,8 +1201,75 @@ export default function HuyaCollabPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const openRecharge = (ids: number[]) => {
|
||||
if (ids.length === 0) return;
|
||||
setRechargeIds(ids);
|
||||
setRechargeThreshold(readRechargeThreshold());
|
||||
setRechargeOpen(true);
|
||||
};
|
||||
|
||||
const submitRecharge = async () => {
|
||||
if (rechargeIds.length === 0) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const threshold = Math.max(0, Number(rechargeThreshold) || 0);
|
||||
try {
|
||||
window.localStorage.setItem(RECHARGE_THRESHOLD_KEY, String(threshold));
|
||||
} catch {
|
||||
// 隐私模式 / 配额满:记不住阈值不影响本次核账
|
||||
}
|
||||
const result = await huyaApi.rechargeCollabOrders({
|
||||
ids: rechargeIds,
|
||||
threshold,
|
||||
});
|
||||
const lines = result.groups.map((group) => ({
|
||||
key: group.group_key,
|
||||
ok: Boolean(group.status),
|
||||
text: group.status
|
||||
? `${group.group_key}:${group.message}`
|
||||
: `${group.group_key}:${group.message}`,
|
||||
}));
|
||||
if (lines.length === 0) {
|
||||
message.warning('没有可核账的订单');
|
||||
} else {
|
||||
modal.info({
|
||||
title: `核账完成:更新 ${result.updated} 条,失败 ${result.failed} 条`,
|
||||
width: 680,
|
||||
content: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{lines.map((line) => (
|
||||
<Text key={line.key} type={line.ok ? undefined : 'danger'}>
|
||||
{line.text}
|
||||
</Text>
|
||||
))}
|
||||
{result.failures.map((item) => (
|
||||
<Text key={`failure-${item.id}`} type="danger">
|
||||
{item.order_no}:{item.message}
|
||||
</Text>
|
||||
))}
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
}
|
||||
setRechargeOpen(false);
|
||||
setSelectedIds([]);
|
||||
if (detail && rechargeIds.includes(detail.id)) {
|
||||
void openDetail(detail);
|
||||
}
|
||||
void load();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const advance = async (row: HuyaCollabOrder) => {
|
||||
if (!row.next_status) return;
|
||||
if (row.next_status === 'recharged') {
|
||||
openRecharge([row.id]);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
try {
|
||||
applyUpdated(await huyaApi.updateCollabOrderStatus(row.id, {
|
||||
@@ -1194,12 +1290,18 @@ export default function HuyaCollabPage() {
|
||||
* 单行状态弹窗的提交(三种模式共用):
|
||||
* - `void`:作废(必须填原因);
|
||||
* - `rollback`:回退到选定状态(`rollback=true`,会清掉后面阶段的时间戳);
|
||||
* - `set`:人工往前切,允许跳步(如 待处理 → 已上脚本),不用勾回退。
|
||||
* - `set`:人工往前切,允许跳步(如 待补资料 → 已上脚本),不用勾回退。
|
||||
*/
|
||||
const submitStatusAction = async () => {
|
||||
if (!statusAction) return;
|
||||
const { order, mode, target } = statusAction;
|
||||
const nextStatus: HuyaCollabStatus = mode === 'void' ? 'void' : (target ?? rollbackTarget);
|
||||
if (nextStatus === 'recharged') {
|
||||
setStatusAction(null);
|
||||
setStatusReason('');
|
||||
openRecharge([order.id]);
|
||||
return;
|
||||
}
|
||||
// 服务层对「作废」一律要求原因(不管从哪个入口进来的)
|
||||
if (nextStatus === 'void' && !statusReason.trim()) {
|
||||
message.warning('作废必须填原因');
|
||||
@@ -1317,6 +1419,7 @@ export default function HuyaCollabPage() {
|
||||
shop: String(values.shop ?? ''),
|
||||
ck_text: String(values.ck_text ?? ''),
|
||||
gift_name: String(values.gift_name ?? ''),
|
||||
required_points: typeof values.required_points === 'number' ? values.required_points : null,
|
||||
remark: String(values.remark ?? ''),
|
||||
home_images: images,
|
||||
};
|
||||
@@ -1324,6 +1427,7 @@ export default function HuyaCollabPage() {
|
||||
payload.ck_text.trim()
|
||||
|| payload.shop !== target.shop
|
||||
|| payload.gift_name !== target.gift_name
|
||||
|| payload.required_points !== target.required_points
|
||||
|| payload.remark !== target.remark
|
||||
|| payload.home_images.join('\n') !== target.home_images.join('\n'),
|
||||
);
|
||||
@@ -1353,6 +1457,7 @@ export default function HuyaCollabPage() {
|
||||
shop: row.shop,
|
||||
ck_text: '',
|
||||
gift_name: row.gift_name,
|
||||
required_points: row.required_points,
|
||||
remark: row.remark,
|
||||
home_images: row.home_images,
|
||||
};
|
||||
@@ -1389,6 +1494,7 @@ export default function HuyaCollabPage() {
|
||||
shop: supplementTarget.shop,
|
||||
ck_text: '',
|
||||
gift_name: supplementTarget.gift_name,
|
||||
required_points: supplementTarget.required_points,
|
||||
remark: supplementTarget.remark,
|
||||
home_images: supplementTarget.home_images,
|
||||
});
|
||||
@@ -1396,7 +1502,7 @@ export default function HuyaCollabPage() {
|
||||
|
||||
/**
|
||||
* 保存「补资料」:字段 / 新 CK / 截图分别走各自接口(每个都有乐观锁),
|
||||
* 新 CK 保存后立即跑一次只读探针;用户点「保存并进入待处理」时再校验资料完整性。
|
||||
* 新 CK 保存后立即跑一次只读探针;用户点「保存并进入待充值」时再校验资料完整性。
|
||||
*/
|
||||
const submitSupplement = async (markPending = false) => {
|
||||
if (!supplementTarget) return;
|
||||
@@ -1412,9 +1518,11 @@ export default function HuyaCollabPage() {
|
||||
const payload: HuyaCollabOrderUpdate = {};
|
||||
const shop = String(values.shop ?? '').trim();
|
||||
const giftName = String(values.gift_name ?? '').trim();
|
||||
const requiredPoints = typeof values.required_points === 'number' ? values.required_points : null;
|
||||
const remark = String(values.remark ?? '');
|
||||
if (shop !== current.shop) payload.shop = shop;
|
||||
if (giftName !== current.gift_name) payload.gift_name = giftName;
|
||||
if (requiredPoints !== current.required_points) payload.required_points = requiredPoints;
|
||||
if (remark !== current.remark) payload.remark = remark;
|
||||
if (Object.keys(payload).length > 0) {
|
||||
current = await huyaApi.updateCollabOrder(current.id, {
|
||||
@@ -1442,7 +1550,7 @@ export default function HuyaCollabPage() {
|
||||
});
|
||||
}
|
||||
|
||||
// 没有换 CK、但准备进「待处理」时,先跑一次只读探针;避免拿一份没验证的旧 CK 往前推
|
||||
// 没有换 CK、但准备检查资料是否齐全时,先跑一次只读探针;避免拿一份没验证的旧 CK 往前推
|
||||
if (markPending && !ckText && current.has_cookie && current.ck_status !== 'valid') {
|
||||
current = await huyaApi.verifyCollabOrder(current.id);
|
||||
}
|
||||
@@ -1453,16 +1561,10 @@ export default function HuyaCollabPage() {
|
||||
supplementForm.setFieldValue('ck_text', '');
|
||||
void load();
|
||||
message.warning(
|
||||
'资料已保存,但这份 CK 还没验证通过,暂时不能进入「待处理」;请先补 / 换一份有效 CK',
|
||||
'资料已保存,但这份 CK 还没验证通过,暂时不能进入「待充值」;请先补 / 换一份有效 CK',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (markPending) {
|
||||
current = await huyaApi.updateCollabOrderStatus(current.id, {
|
||||
status: 'pending',
|
||||
expected_version: current.version,
|
||||
});
|
||||
}
|
||||
|
||||
applyUpdated(current);
|
||||
clearSupplementDraft(user?.id, supplementTarget.id);
|
||||
@@ -1471,7 +1573,9 @@ export default function HuyaCollabPage() {
|
||||
setSupplementTarget(null);
|
||||
message.success(
|
||||
markPending
|
||||
? `已补齐资料,${current.order_no} 进入「待处理」`
|
||||
? current.status === 'to_recharge'
|
||||
? `资料已补齐,${current.order_no} 已进入「待充值」`
|
||||
: `资料已保存;绑定账号/角色、礼包、主页截图齐全后会自动进入「待充值」`
|
||||
: `已保存 ${current.order_no} 的资料`,
|
||||
);
|
||||
void load();
|
||||
@@ -1541,6 +1645,7 @@ export default function HuyaCollabPage() {
|
||||
shop: String(values.shop ?? ''),
|
||||
ck_text: String(values.ck_text ?? ''),
|
||||
gift_name: String(values.gift_name ?? ''),
|
||||
required_points: typeof values.required_points === 'number' ? values.required_points : null,
|
||||
remark: String(values.remark ?? ''),
|
||||
home_images: Array.isArray(values.home_images)
|
||||
? (values.home_images as unknown[]).filter((item): item is string => typeof item === 'string')
|
||||
@@ -1600,6 +1705,7 @@ export default function HuyaCollabPage() {
|
||||
const orderNo = String(values.order_no ?? '').trim();
|
||||
const ckText = String(values.ck_text ?? '').trim();
|
||||
const giftName = String(values.gift_name ?? '').trim();
|
||||
const requiredPoints = typeof values.required_points === 'number' ? values.required_points : null;
|
||||
const homeImages = (values.home_images as string[] | undefined) ?? [];
|
||||
const missing: string[] = [];
|
||||
if (!ckText) missing.push('外部 CK');
|
||||
@@ -1613,6 +1719,7 @@ export default function HuyaCollabPage() {
|
||||
ck_text: ckText,
|
||||
// 「绑定斗鱼号」不再录入:绑了谁由探针读(bind_game_*),人工那一列已下线
|
||||
gift_name: giftName,
|
||||
required_points: requiredPoints,
|
||||
home_images: homeImages,
|
||||
remark: String(values.remark ?? ''),
|
||||
draft,
|
||||
@@ -1627,7 +1734,7 @@ export default function HuyaCollabPage() {
|
||||
// 首次建草稿单给一次明确提示:以后直接点「新建」就行,不用先勾任何东西
|
||||
message.info(
|
||||
`已按「待补资料」创建 ${created.order_no}:${reason}。`
|
||||
+ '之后在详情里补齐,再点「标记为待处理」即可。',
|
||||
+ '之后在详情里补齐,资料齐全并验证 CK 后会自动进入「待充值」。',
|
||||
8,
|
||||
);
|
||||
} else {
|
||||
@@ -1685,6 +1792,32 @@ export default function HuyaCollabPage() {
|
||||
// 筛选里的店铺也是点选(含停用店铺:历史订单还在用它们)
|
||||
const finished = summary ? (summary.by_status.done ?? 0) : 0;
|
||||
|
||||
/** 顶部统计卡点一下 = 筛到对应状态,并滚回列表顶部。 */
|
||||
const scrollToList = () => {
|
||||
window.setTimeout(() => {
|
||||
listAnchorRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 0);
|
||||
};
|
||||
const applyStatusCard = (status?: HuyaCollabStatus) => {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, status, ck_status: undefined }));
|
||||
scrollToList();
|
||||
};
|
||||
const applyCkInvalidCard = () => {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, status: undefined, ck_status: 'invalid' }));
|
||||
scrollToList();
|
||||
};
|
||||
const statusCardActive = (status: HuyaCollabStatus) =>
|
||||
filters.status === status && !filters.ck_status;
|
||||
const allCardActive = !filters.status && !filters.ck_status;
|
||||
const ckInvalidCardActive = filters.ck_status === 'invalid';
|
||||
const cardStyle = (active: boolean) => ({
|
||||
cursor: 'pointer',
|
||||
borderColor: active ? '#1677ff' : undefined,
|
||||
boxShadow: active ? '0 0 0 2px rgba(22, 119, 255, 0.16)' : undefined,
|
||||
});
|
||||
|
||||
// 礼包来源提示:这次真去虎牙拉了就说拉了,用的是缓存就把缓存时间摆出来(让人知道要不要点强制刷新)
|
||||
const giftCacheLabel = giftsCachedAt ? formatTime(new Date(giftsCachedAt).toISOString()) : '';
|
||||
const giftSourceText = ckPreview?.gifts?.length
|
||||
@@ -1714,6 +1847,13 @@ export default function HuyaCollabPage() {
|
||||
return options;
|
||||
};
|
||||
|
||||
/** 礼包名对应的积分单价;缓存里没有 / 没价格时返回 null(后端核账时还会按名称补一次)。 */
|
||||
const giftRequiredPoints = (name?: string | null): number | null => {
|
||||
const value = String(name ?? '').trim();
|
||||
if (!value) return null;
|
||||
return giftOptions.find((gift) => gift.name === value)?.price ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 「常用礼包」快捷按钮(2026-10-18 用户要求):
|
||||
* 礼包名**默认空白**(不再自动带上次选的那个,避免没注意时建错单),
|
||||
@@ -1968,7 +2108,10 @@ export default function HuyaCollabPage() {
|
||||
labelRender={() => renderGiftSelectedLabel(row.gift_name)}
|
||||
styles={{ content: { whiteSpace: 'normal', textOverflow: 'clip', lineHeight: 1.3 } }}
|
||||
notFoundContent={giftOptions.length === 0 ? '礼包缓存为空:先在「新建」里贴 CK 拉一次' : '没有匹配的礼包'}
|
||||
onChange={(next: string) => void patchOrder(row, { gift_name: next })}
|
||||
onChange={(next: string) => void patchOrder(row, {
|
||||
gift_name: next,
|
||||
required_points: giftRequiredPoints(next),
|
||||
})}
|
||||
/>
|
||||
<EditableText value={row.remark} disabled={!canEdit || row.is_archived} placeholder="+ 备注"
|
||||
onCommit={(next) => void patchOrder(row, { remark: next })} />
|
||||
@@ -2128,7 +2271,7 @@ export default function HuyaCollabPage() {
|
||||
setStatusAction({ order: row, mode: 'set' });
|
||||
}
|
||||
if (key === 'rollback') {
|
||||
setRollbackTarget('pending');
|
||||
setRollbackTarget('to_recharge');
|
||||
setStatusReason('');
|
||||
setStatusAction({ order: row, mode: 'rollback' });
|
||||
}
|
||||
@@ -2157,52 +2300,42 @@ export default function HuyaCollabPage() {
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
{/* 卡片用 flex 平分:状态多了一个「抢购失败」,固定 span=4 会换行参差不齐 */}
|
||||
<Row gutter={12}>
|
||||
<Col flex="1"><Card size="small"><Statistic title="全部" value={summary?.total ?? 0} /></Card></Col>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title={STATUS_META.incomplete.label} value={summary?.by_status.incomplete ?? 0}
|
||||
valueStyle={{ color: '#fa8c16' }} />
|
||||
{/* 卡片用 flex 平分并允许换行:状态多,固定 span 会参差不齐 */}
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col flex="1 1 104px">
|
||||
<Card size="small" hoverable style={cardStyle(allCardActive)} onClick={() => applyStatusCard(undefined)}>
|
||||
<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 flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title={STATUS_META.bound.label} value={summary?.by_status.bound ?? 0}
|
||||
valueStyle={{ color: '#1677ff' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title={STATUS_META.recharged.label} value={summary?.by_status.recharged ?? 0}
|
||||
valueStyle={{ color: '#2f54eb' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title={STATUS_META.scripted.label} value={summary?.by_status.scripted ?? 0}
|
||||
valueStyle={{ color: '#722ed1' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Card size="small">
|
||||
<Statistic title={STATUS_META.done.label} value={finished} valueStyle={{ color: '#52c41a' }} />
|
||||
<Progress percent={summary?.total ? Math.round((finished / summary.total) * 100) : 0} size="small" />
|
||||
</Card>
|
||||
</Col>
|
||||
<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">
|
||||
{([
|
||||
['incomplete', '#fa8c16'],
|
||||
['to_recharge', '#faad14'],
|
||||
['recharged', '#2f54eb'],
|
||||
['scripted', '#722ed1'],
|
||||
['done', '#52c41a'],
|
||||
['failed', '#ff4d4f'],
|
||||
['points_error', '#fa541c'],
|
||||
] as const).map(([status, color]) => (
|
||||
<Col key={status} flex="1 1 104px">
|
||||
<Card
|
||||
size="small"
|
||||
hoverable
|
||||
style={cardStyle(statusCardActive(status))}
|
||||
onClick={() => applyStatusCard(status)}
|
||||
>
|
||||
<Statistic
|
||||
title={STATUS_META[status].label}
|
||||
value={status === 'done' ? finished : (summary?.by_status[status] ?? 0)}
|
||||
valueStyle={{ color }}
|
||||
/>
|
||||
{status === 'done' && (
|
||||
<Progress percent={summary?.total ? Math.round((finished / summary.total) * 100) : 0} size="small" />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
<Col flex="1 1 104px">
|
||||
<Card size="small" hoverable style={cardStyle(ckInvalidCardActive)} onClick={applyCkInvalidCard}>
|
||||
<Statistic title="CK 失效" value={summary?.ck_invalid ?? 0} valueStyle={{ color: '#ff4d4f' }} />
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -2245,6 +2378,7 @@ export default function HuyaCollabPage() {
|
||||
)}
|
||||
</Row>
|
||||
|
||||
<div ref={listAnchorRef} style={{ scrollMarginTop: 8 }} />
|
||||
<Card size="small">
|
||||
{/*
|
||||
固定两行:第一行永远是筛选,第二行永远是动作按钮组 —— 不要用「筛选 + 动作同一个 wrap」
|
||||
@@ -2353,6 +2487,14 @@ export default function HuyaCollabPage() {
|
||||
</Button>
|
||||
</Dropdown>
|
||||
)}
|
||||
{canEdit && !isArchiveView && (
|
||||
<Tooltip title="按绑定账号整组校验 CK 和积分,整组落到「已充值 / 积分异常」">
|
||||
<Button icon={<AuditOutlined />} disabled={selectedIds.length === 0 || busy}
|
||||
onClick={() => openRecharge(selectedIds)}>
|
||||
充值核账{selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canEdit && !isArchiveView && (
|
||||
// 查积分 / 查兑换记录**拆成两个按钮**(2026-10-17):查积分只写积分快照,
|
||||
// 既不判定也不自动改状态;只有查兑换记录才产出判定并可能自动改状态。
|
||||
@@ -2401,7 +2543,7 @@ export default function HuyaCollabPage() {
|
||||
// 上次填了一半:订单号 / 店铺 / CK / 截图 / 备注都原样恢复,并重新验证暂存的 CK。
|
||||
// **礼包名一律留空**(2026-10-18 用户要求):礼包默认空白,不再带上次选的那个;
|
||||
// 反正上面有「常用礼包」备选,点一下就有,不值得为它记住上一次的选择。
|
||||
createForm.setFieldsValue({ ...draft, gift_name: '' });
|
||||
createForm.setFieldsValue({ ...draft, gift_name: '', required_points: null });
|
||||
setGiftHint('');
|
||||
setDraftRestoredAt(draft.saved_at || Date.now());
|
||||
setCreateOpen(true);
|
||||
@@ -2506,15 +2648,27 @@ export default function HuyaCollabPage() {
|
||||
description={(() => {
|
||||
const missing = [
|
||||
!detail.has_cookie ? '外部 CK' : '',
|
||||
!detail.bind_game_account ? '绑定账号(先验证 CK)' : '',
|
||||
!detail.bind_game_role ? '绑定角色(先验证 CK)' : '',
|
||||
!detail.gift_name ? '礼包名' : '',
|
||||
detail.home_images.length === 0 ? '主页截图' : '',
|
||||
].filter(Boolean);
|
||||
return missing.length > 0
|
||||
? `还缺:${missing.join('、')}。补齐后点下面的「标记为待处理」按钮即可进入正常流程。`
|
||||
: '资料已齐,点下面的「标记为待处理」按钮进入正常流程。';
|
||||
? `还缺:${missing.join('、')}。资料齐全且 CK 有效后会自动进入「待充值」。`
|
||||
: '资料已齐,验证 CK 后会自动进入「待充值」。';
|
||||
})()}
|
||||
/>
|
||||
)}
|
||||
{detail.status === 'points_error' && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={detail.points_check_message || '积分异常:账号积分与礼包所需积分不匹配'}
|
||||
description={detail.points_checked_at
|
||||
? `最近核账 ${formatTime(detail.points_checked_at)};如已补足/调整积分,重新勾选后点「充值核账」。`
|
||||
: '请重新勾选后点「充值核账」。'}
|
||||
/>
|
||||
)}
|
||||
<Descriptions size="small" column={2} bordered
|
||||
items={[
|
||||
{ key: 'uid', label: '虎牙号', children: detail.uid || '-' },
|
||||
@@ -2532,6 +2686,11 @@ export default function HuyaCollabPage() {
|
||||
{ key: 'game_role', label: '绑定角色(探针)', children: detail.bind_game_role || '-' },
|
||||
{ key: 'game_channel', label: '平台 / 区服', children: detail.bind_game_channel || '-' },
|
||||
{ key: 'snapshot_at', label: '绑定快照时间', children: formatTime(detail.bind_snapshot_at) },
|
||||
{ key: 'gift', label: '礼包 / 所需积分', children: `${detail.gift_name || '-'} / ${detail.required_points ?? '未记录'}` },
|
||||
{ key: 'points_check', label: '最近核账(账号积分 / 礼包合计 / 结余 / 阈值)',
|
||||
children: detail.points_checked_at
|
||||
? `${detail.points_balance ?? '-'} / ${detail.points_required_total ?? '-'} / ${detail.points_surplus ?? '-'} / ${detail.points_threshold ?? '-'}(${formatTime(detail.points_checked_at)})`
|
||||
: '-' },
|
||||
{ key: 'status', label: '业务状态',
|
||||
children: <Tag color={STATUS_META[detail.status]?.color}>{detail.status_label}</Tag> },
|
||||
...(detail.is_archived
|
||||
@@ -2595,7 +2754,7 @@ export default function HuyaCollabPage() {
|
||||
size="small"
|
||||
initialValues={{
|
||||
order_no: detail.order_no, shop: detail.shop, owner_id: detail.owner_id,
|
||||
gift_name: detail.gift_name, remark: detail.remark,
|
||||
gift_name: detail.gift_name, required_points: detail.required_points, remark: detail.remark,
|
||||
}}
|
||||
onFinish={(values) => void patchOrder(detail, values)}
|
||||
>
|
||||
@@ -2627,9 +2786,19 @@ export default function HuyaCollabPage() {
|
||||
notFoundContent={giftOptions.length === 0
|
||||
? '礼包缓存为空:先在「新建」里贴 CK 拉一次'
|
||||
: '没有匹配的礼包'}
|
||||
onChange={(value?: string) => {
|
||||
detailForm.setFieldValue('required_points', giftRequiredPoints(value));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="required_points" label="礼包所需积分"
|
||||
tooltip="选中礼包时自动带出积分中心价格;也可按实际活动手工修正,核账时按这个值合计">
|
||||
<InputNumber min={0} max={1_000_000} precision={0} style={{ width: '100%' }}
|
||||
addonAfter="积分" placeholder="未记录时核账会按礼包名自动补" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Form.Item name="remark" label="备注"><Input.TextArea rows={2} maxLength={512} /></Form.Item>
|
||||
</Col>
|
||||
@@ -2702,7 +2871,7 @@ export default function HuyaCollabPage() {
|
||||
改状态…
|
||||
</Button>
|
||||
<Button icon={<UndoOutlined />} disabled={busy}
|
||||
onClick={() => { setRollbackTarget('pending'); setStatusReason(''); setStatusAction({ order: detail, mode: 'rollback' }); }}>
|
||||
onClick={() => { setRollbackTarget('to_recharge'); setStatusReason(''); setStatusAction({ order: detail, mode: 'rollback' }); }}>
|
||||
回退状态
|
||||
</Button>
|
||||
{detail.status !== 'void' && (
|
||||
@@ -2939,7 +3108,10 @@ export default function HuyaCollabPage() {
|
||||
<Row gutter={16}>
|
||||
<Col span={14}>
|
||||
{renderFrequentGiftChips(createGiftValue, (name) => {
|
||||
createForm.setFieldsValue({ gift_name: name });
|
||||
createForm.setFieldsValue({
|
||||
gift_name: name,
|
||||
required_points: giftRequiredPoints(name),
|
||||
});
|
||||
setGiftHint('');
|
||||
})}
|
||||
<Form.Item
|
||||
@@ -2959,12 +3131,21 @@ export default function HuyaCollabPage() {
|
||||
options={giftSelectOptions(createGiftValue)}
|
||||
disabled={giftOptions.length === 0 && !createGiftValue}
|
||||
notFoundContent={giftOptions.length === 0 ? '先在左侧贴一份有效 CK 拉取礼包列表' : '没有匹配的礼包'}
|
||||
onChange={() => {
|
||||
// 选了就清掉「上次那个礼包不在本次列表里」的提示
|
||||
onChange={(value?: string) => {
|
||||
// 选了就清掉「上次那个礼包不在本次列表里」的提示,并带出积分价格
|
||||
setGiftHint('');
|
||||
createForm.setFieldValue('required_points', giftRequiredPoints(value));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="required_points"
|
||||
label="礼包所需积分(可稍后补)"
|
||||
tooltip="选择礼包时自动带出积分中心价格;核账时按每单这个值合计。留空也能核账,后端会按礼包名补一次"
|
||||
>
|
||||
<InputNumber min={0} max={1_000_000} precision={0} style={{ width: '100%' }}
|
||||
addonAfter="积分" placeholder="选中礼包自动带出" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Form.Item
|
||||
@@ -3015,7 +3196,7 @@ export default function HuyaCollabPage() {
|
||||
保存资料
|
||||
</Button>,
|
||||
<Button key="pending" type="primary" onClick={() => void submitSupplement(true)} loading={busy}>
|
||||
保存并进入待处理
|
||||
保存并检查资料
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
@@ -3040,10 +3221,14 @@ export default function HuyaCollabPage() {
|
||||
const images = supplementHomeImages ?? supplementTarget.home_images;
|
||||
const missing = [
|
||||
!hasCk ? '外部 CK' : '',
|
||||
!supplementTarget.bind_game_account ? '绑定账号(保存后验证 CK)' : '',
|
||||
!supplementTarget.bind_game_role ? '绑定角色(保存后验证 CK)' : '',
|
||||
!supplementGiftValue ? '礼包名' : '',
|
||||
images.length === 0 ? '主页截图' : '',
|
||||
].filter(Boolean);
|
||||
return missing.length > 0 ? `当前还缺:${missing.join('、')}` : '资料已齐,可以直接「保存并进入待处理」。';
|
||||
return missing.length > 0
|
||||
? `当前还缺:${missing.join('、')}`
|
||||
: '资料已齐,保存并验证 CK 后会自动进入「待充值」。';
|
||||
})()}
|
||||
/>
|
||||
<Form
|
||||
@@ -3065,7 +3250,10 @@ export default function HuyaCollabPage() {
|
||||
<Col span={12}>
|
||||
{renderFrequentGiftChips(
|
||||
supplementGiftValue ?? supplementTarget.gift_name,
|
||||
(name) => supplementForm.setFieldsValue({ gift_name: name }),
|
||||
(name) => supplementForm.setFieldsValue({
|
||||
gift_name: name,
|
||||
required_points: giftRequiredPoints(name),
|
||||
}),
|
||||
)}
|
||||
<Form.Item name="gift_name" label="礼包名"
|
||||
tooltip="只接受礼包缓存里的点选结果,可输入关键字搜索;不接受手工输入">
|
||||
@@ -3079,8 +3267,16 @@ export default function HuyaCollabPage() {
|
||||
notFoundContent={giftOptions.length === 0
|
||||
? '礼包缓存为空:先在「新建」里贴 CK 拉一次'
|
||||
: '没有匹配的礼包'}
|
||||
onChange={(value?: string) => {
|
||||
supplementForm.setFieldValue('required_points', giftRequiredPoints(value));
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="required_points" label="礼包所需积分"
|
||||
tooltip="选中礼包时自动带出积分中心价格;核账时按每单这个值合计">
|
||||
<InputNumber min={0} max={1_000_000} precision={0} style={{ width: '100%' }}
|
||||
addonAfter="积分" placeholder="选中礼包自动带出" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item
|
||||
@@ -3190,6 +3386,41 @@ export default function HuyaCollabPage() {
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`充值核账(已选 ${rechargeIds.length} 条)`}
|
||||
open={rechargeOpen}
|
||||
onCancel={() => setRechargeOpen(false)}
|
||||
onOk={() => void submitRecharge()}
|
||||
confirmLoading={busy}
|
||||
okText="核账并落状态"
|
||||
destroyOnHidden
|
||||
>
|
||||
<Space direction="vertical" size={10} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="按绑定账号整组核账"
|
||||
description="同一 bind_game_account 下的待充值/已充值/积分异常订单会一起核账;CK 失效或积分查不到时整组保持原状态。"
|
||||
/>
|
||||
<Space size={8} align="center">
|
||||
<Text>允许结余阈值</Text>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1_000_000}
|
||||
precision={0}
|
||||
value={rechargeThreshold}
|
||||
onChange={(value) => setRechargeThreshold(Number(value) || 0)}
|
||||
addonAfter="积分"
|
||||
style={{ width: 160 }}
|
||||
/>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
结余 = 账号积分合计 - 礼包积分合计。结余小于 0 或大于阈值 → 整组「积分异常」;
|
||||
其余 → 整组「已充值」。阈值默认 100。
|
||||
</Text>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={statusAction?.mode === 'void'
|
||||
? `作废订单 ${statusAction.order.order_no}`
|
||||
@@ -3225,8 +3456,8 @@ export default function HuyaCollabPage() {
|
||||
} />
|
||||
{statusAction?.mode === 'set' && (
|
||||
<Text type="secondary">
|
||||
人工切换允许往前跳步(比如 待处理 → 已上脚本),不用一步一步点;
|
||||
切到「已确认绑定」要求先有绑定角色 + 主页截图,切到「已作废」必须填原因。
|
||||
人工切换允许往前跳步(比如 待补资料 → 已上脚本),不用一步一步点;
|
||||
选「已充值」会转为整组核账;切到「已作废」必须填原因。
|
||||
</Text>
|
||||
)}
|
||||
{statusAction?.mode === 'rollback' && (
|
||||
@@ -3287,10 +3518,10 @@ export default function HuyaCollabPage() {
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user