优化电竞手册换绑时间展示
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""新增电竞手册可换绑时间
|
||||
|
||||
Revision ID: 20260728_0012
|
||||
Revises: 20260728_0011
|
||||
Create Date: 2026-07-28
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260728_0012"
|
||||
down_revision: Union[str, None] = "20260728_0011"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {column["name"] for column in sa.inspect(bind).get_columns("accounts")}
|
||||
if "esports_can_change_time" not in columns:
|
||||
op.add_column("accounts", sa.Column("esports_can_change_time", sa.Integer(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {column["name"] for column in sa.inspect(bind).get_columns("accounts")}
|
||||
if "esports_can_change_time" in columns:
|
||||
op.drop_column("accounts", "esports_can_change_time")
|
||||
@@ -69,6 +69,7 @@ class Account(Base):
|
||||
esports_game_channel = Column(String(128), default="")
|
||||
esports_bind_status = Column(String(32), default="")
|
||||
esports_change_role_wait_time = Column(Integer, nullable=True)
|
||||
esports_can_change_time = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ def _account_out(account: Account) -> DouyuTaskAccountOut:
|
||||
esports_game_channel=account.esports_game_channel or "",
|
||||
esports_bind_status=account.esports_bind_status or "",
|
||||
esports_change_role_wait_time=account.esports_change_role_wait_time,
|
||||
esports_can_change_time=account.esports_can_change_time,
|
||||
assigned_to=account.assigned_to,
|
||||
assigned_username=account.assigned_user.username if account.assigned_user else None,
|
||||
)
|
||||
|
||||
@@ -671,6 +671,7 @@ class DouyuTaskAccountOut(BaseModel):
|
||||
esports_game_channel: str = ""
|
||||
esports_bind_status: str = ""
|
||||
esports_change_role_wait_time: Optional[int] = None
|
||||
esports_can_change_time: Optional[int] = None
|
||||
assigned_to: Optional[int] = None
|
||||
assigned_username: Optional[str] = None
|
||||
|
||||
|
||||
@@ -174,6 +174,10 @@ class DouyuBatchRunner:
|
||||
"""综合 can_change_role 与换绑倒计时判断是否允许换绑。"""
|
||||
if not bind_info:
|
||||
return True
|
||||
if str(bind_info.get("api_version") or "") == "esports":
|
||||
can_change_time = cls._to_int(bind_info.get("can_change_time"))
|
||||
if can_change_time is not None:
|
||||
return can_change_time <= int(datetime.now(timezone.utc).timestamp())
|
||||
wait_time = cls._to_int(bind_info.get("change_role_wait_time"))
|
||||
if wait_time is not None and wait_time > 0:
|
||||
return False
|
||||
@@ -247,6 +251,7 @@ class DouyuBatchRunner:
|
||||
"need_bind_act": self._is_truthy_flag(info.get("need_bind_act")),
|
||||
"need_bind_role": self._is_truthy_flag(info.get("need_bind_role")),
|
||||
"change_role_wait_time": self._to_int(info.get("change_role_wait_time")),
|
||||
"can_change_time": self._to_int(info.get("can_change_time")),
|
||||
"can_change_role": self._can_change_role(info),
|
||||
"bind_info": info,
|
||||
}
|
||||
@@ -266,6 +271,7 @@ class DouyuBatchRunner:
|
||||
f" need_act={1 if snap['need_bind_act'] else 0}"
|
||||
f" need_role={1 if snap['need_bind_role'] else 0}"
|
||||
f" wait={snap['change_role_wait_time'] if snap['change_role_wait_time'] is not None else '-'}"
|
||||
f" can_change_time={snap['can_change_time'] if snap['can_change_time'] is not None else '-'}"
|
||||
f"{pending_text}"
|
||||
)
|
||||
|
||||
@@ -283,6 +289,7 @@ class DouyuBatchRunner:
|
||||
account.esports_game_name = role_name or account.esports_game_name
|
||||
account.esports_game_channel = self._role_channel(bind_info) or account.esports_game_channel
|
||||
account.esports_change_role_wait_time = self._to_int(bind_info.get("change_role_wait_time"))
|
||||
account.esports_can_change_time = self._to_int(bind_info.get("can_change_time"))
|
||||
account.esports_bind_status = status
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ export interface DouyuTaskAccountItem {
|
||||
esports_game_channel: string;
|
||||
esports_bind_status: string;
|
||||
esports_change_role_wait_time: number | null;
|
||||
esports_can_change_time: number | null;
|
||||
assigned_to: number | null;
|
||||
assigned_username: string | null;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,21 @@ function formatWaitSeconds(seconds: number | null | undefined): string {
|
||||
return `${minutes}分${secs}秒`;
|
||||
}
|
||||
|
||||
function esportsRebindStatus(canChangeTime: number | null | undefined): {
|
||||
available: boolean;
|
||||
text: string;
|
||||
} | null {
|
||||
if (canChangeTime == null || Number.isNaN(Number(canChangeTime))) return null;
|
||||
const timestamp = Math.floor(Number(canChangeTime));
|
||||
if (timestamp <= 0 || timestamp <= Math.floor(Date.now() / 1000)) {
|
||||
return { available: true, text: '可换绑' };
|
||||
}
|
||||
return {
|
||||
available: false,
|
||||
text: new Date(timestamp * 1000).toLocaleString('zh-CN', { hour12: false }),
|
||||
};
|
||||
}
|
||||
|
||||
function resultNumber(result: Record<string, unknown> | null | undefined, key: string): number | null {
|
||||
const value = result?.[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
@@ -267,7 +282,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
|
||||
// 换绑时间:优先最近一次查询/拦截任务结果,回退账号落库字段
|
||||
const latestChangeWaitByAccount = useMemo(() => {
|
||||
const map = new Map<number, { wait: number | null; text: string; taskId: number }>();
|
||||
const map = new Map<number, {
|
||||
wait: number | null; canChangeTime: number | null; text: string; taskId: number;
|
||||
}>();
|
||||
for (const task of tasks) {
|
||||
const taskTypes = isEsportsHandbook
|
||||
? ['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
|
||||
@@ -275,12 +292,14 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
if (!taskTypes.includes(task.task_type)) continue;
|
||||
if (!['success', 'failed'].includes(task.status)) continue;
|
||||
const wait = resultNumber(task.result, 'change_role_wait_time');
|
||||
const canChangeTime = resultNumber(task.result, 'can_change_time');
|
||||
// get_bind_qr 只有冷却拦截时才有 wait;无 wait 时跳过,避免覆盖
|
||||
if (!isEsportsHandbook && task.task_type === 'get_bind_qr' && wait == null) continue;
|
||||
const existing = map.get(task.account_id);
|
||||
if (existing && existing.taskId > task.id) continue;
|
||||
const text = resultText(task.result, 'change_role_wait_text') || formatWaitSeconds(wait);
|
||||
map.set(task.account_id, { wait, text, taskId: task.id });
|
||||
const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null;
|
||||
const text = esportsStatus?.text || resultText(task.result, 'change_role_wait_text') || formatWaitSeconds(wait);
|
||||
map.set(task.account_id, { wait, canChangeTime, text, taskId: task.id });
|
||||
}
|
||||
return map;
|
||||
}, [isEsportsHandbook, tasks]);
|
||||
@@ -610,7 +629,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
const esportsPlatName = resultText(esportsResult, 'plat_name');
|
||||
const esportsRoleLine = [esportsPlatName, esportsAreaName].filter(Boolean).join(' / ');
|
||||
const esportsCanChangeRole = Boolean(esportsBindTask && !esportsConfirmRunning);
|
||||
const esportsChangeWait = resultNumber(esportsResult, 'change_role_wait_time');
|
||||
const esportsCanChangeTime = resultNumber(esportsResult, 'can_change_time');
|
||||
const esportsRebind = esportsRebindStatus(esportsCanChangeTime);
|
||||
const esportsQrUrl = resultText(esportsLatestQrTask?.result, 'url');
|
||||
const esportsAccountName = esportsBindTask
|
||||
? (
|
||||
@@ -786,6 +806,20 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
|
||||
render: (_, record) => {
|
||||
const fromTask = latestChangeWaitByAccount.get(record.id);
|
||||
const canChangeTime = fromTask?.canChangeTime ?? (
|
||||
isEsportsHandbook ? record.esports_can_change_time : null
|
||||
);
|
||||
const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null;
|
||||
if (esportsStatus) {
|
||||
return esportsStatus.available
|
||||
? <Tag color="success">可换绑</Tag>
|
||||
: (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Tag color="orange">暂不可换绑</Tag>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{esportsStatus.text}</Text>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
const wait = fromTask?.wait ?? (
|
||||
isEsportsHandbook ? record.esports_change_role_wait_time : record.change_role_wait_time
|
||||
);
|
||||
@@ -809,9 +843,13 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
return <Text type="secondary">{text || '-'}</Text>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
const aw = latestChangeWaitByAccount.get(a.id)?.wait
|
||||
const aw = isEsportsHandbook
|
||||
? latestChangeWaitByAccount.get(a.id)?.canChangeTime ?? a.esports_can_change_time ?? -1
|
||||
: latestChangeWaitByAccount.get(a.id)?.wait
|
||||
?? (isEsportsHandbook ? a.esports_change_role_wait_time : a.change_role_wait_time) ?? -1;
|
||||
const bw = latestChangeWaitByAccount.get(b.id)?.wait
|
||||
const bw = isEsportsHandbook
|
||||
? latestChangeWaitByAccount.get(b.id)?.canChangeTime ?? b.esports_can_change_time ?? -1
|
||||
: latestChangeWaitByAccount.get(b.id)?.wait
|
||||
?? (isEsportsHandbook ? b.esports_change_role_wait_time : b.change_role_wait_time) ?? -1;
|
||||
return aw - bw;
|
||||
},
|
||||
@@ -1391,8 +1429,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
<Text type="secondary">当前未检测到游戏角色,请先切换角色。</Text>
|
||||
)}
|
||||
|
||||
{esportsChangeWait != null && esportsChangeWait > 0 && esportsRoleName ? (
|
||||
<Text type="secondary">换绑冷却剩余:{formatWaitSeconds(esportsChangeWait)}</Text>
|
||||
{esportsRebind && esportsRoleName ? (
|
||||
<Text type="secondary">
|
||||
{esportsRebind.available ? '当前可换绑' : `可换绑时间:${esportsRebind.text}`}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{esportsQrUrl ? (
|
||||
|
||||
Reference in New Issue
Block a user