优化电竞手册换绑时间展示
This commit is contained in:
@@ -279,6 +279,13 @@ class DouyuActivityClient:
|
|||||||
)
|
)
|
||||||
if can_change_role is None and isinstance(tx_act, dict):
|
if can_change_role is None and isinstance(tx_act, dict):
|
||||||
can_change_role = self._first_present(tx_act, "canChangeRole", "canChangeBind")
|
can_change_role = self._first_present(tx_act, "canChangeRole", "canChangeBind")
|
||||||
|
can_change_time = self._first_present(
|
||||||
|
data,
|
||||||
|
"canChangeTime",
|
||||||
|
"can_change_time",
|
||||||
|
)
|
||||||
|
if can_change_time is None and isinstance(tx_act, dict):
|
||||||
|
can_change_time = self._first_present(tx_act, "canChangeTime", "can_change_time")
|
||||||
return {
|
return {
|
||||||
"act_alias": act_alias,
|
"act_alias": act_alias,
|
||||||
"api_version": api_version,
|
"api_version": api_version,
|
||||||
@@ -297,6 +304,7 @@ class DouyuActivityClient:
|
|||||||
"tx_act": tx_act if isinstance(tx_act, dict) else {},
|
"tx_act": tx_act if isinstance(tx_act, dict) else {},
|
||||||
"change_role_wait_time": change_role_wait_time,
|
"change_role_wait_time": change_role_wait_time,
|
||||||
"can_change_role": can_change_role,
|
"can_change_role": can_change_role,
|
||||||
|
"can_change_time": can_change_time,
|
||||||
"raw": payload,
|
"raw": payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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_game_channel = Column(String(128), default="")
|
||||||
esports_bind_status = Column(String(32), default="")
|
esports_bind_status = Column(String(32), default="")
|
||||||
esports_change_role_wait_time = Column(Integer, nullable=True)
|
esports_change_role_wait_time = Column(Integer, nullable=True)
|
||||||
|
esports_can_change_time = Column(Integer, nullable=True)
|
||||||
created_at = Column(DateTime, default=_utcnow)
|
created_at = Column(DateTime, default=_utcnow)
|
||||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_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_game_channel=account.esports_game_channel or "",
|
||||||
esports_bind_status=account.esports_bind_status or "",
|
esports_bind_status=account.esports_bind_status or "",
|
||||||
esports_change_role_wait_time=account.esports_change_role_wait_time,
|
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_to=account.assigned_to,
|
||||||
assigned_username=account.assigned_user.username if account.assigned_user else None,
|
assigned_username=account.assigned_user.username if account.assigned_user else None,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -671,6 +671,7 @@ class DouyuTaskAccountOut(BaseModel):
|
|||||||
esports_game_channel: str = ""
|
esports_game_channel: str = ""
|
||||||
esports_bind_status: str = ""
|
esports_bind_status: str = ""
|
||||||
esports_change_role_wait_time: Optional[int] = None
|
esports_change_role_wait_time: Optional[int] = None
|
||||||
|
esports_can_change_time: Optional[int] = None
|
||||||
assigned_to: Optional[int] = None
|
assigned_to: Optional[int] = None
|
||||||
assigned_username: Optional[str] = None
|
assigned_username: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ class DouyuBatchRunner:
|
|||||||
"""综合 can_change_role 与换绑倒计时判断是否允许换绑。"""
|
"""综合 can_change_role 与换绑倒计时判断是否允许换绑。"""
|
||||||
if not bind_info:
|
if not bind_info:
|
||||||
return True
|
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"))
|
wait_time = cls._to_int(bind_info.get("change_role_wait_time"))
|
||||||
if wait_time is not None and wait_time > 0:
|
if wait_time is not None and wait_time > 0:
|
||||||
return False
|
return False
|
||||||
@@ -247,6 +251,7 @@ class DouyuBatchRunner:
|
|||||||
"need_bind_act": self._is_truthy_flag(info.get("need_bind_act")),
|
"need_bind_act": self._is_truthy_flag(info.get("need_bind_act")),
|
||||||
"need_bind_role": self._is_truthy_flag(info.get("need_bind_role")),
|
"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")),
|
"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),
|
"can_change_role": self._can_change_role(info),
|
||||||
"bind_info": info,
|
"bind_info": info,
|
||||||
}
|
}
|
||||||
@@ -266,6 +271,7 @@ class DouyuBatchRunner:
|
|||||||
f" need_act={1 if snap['need_bind_act'] else 0}"
|
f" need_act={1 if snap['need_bind_act'] else 0}"
|
||||||
f" need_role={1 if snap['need_bind_role'] 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" 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}"
|
f"{pending_text}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -283,6 +289,7 @@ class DouyuBatchRunner:
|
|||||||
account.esports_game_name = role_name or account.esports_game_name
|
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_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_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.esports_bind_status = status
|
||||||
account.updated_at = datetime.now(timezone.utc)
|
account.updated_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ export interface DouyuTaskAccountItem {
|
|||||||
esports_game_channel: string;
|
esports_game_channel: string;
|
||||||
esports_bind_status: string;
|
esports_bind_status: string;
|
||||||
esports_change_role_wait_time: number | null;
|
esports_change_role_wait_time: number | null;
|
||||||
|
esports_can_change_time: number | null;
|
||||||
assigned_to: number | null;
|
assigned_to: number | null;
|
||||||
assigned_username: string | null;
|
assigned_username: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,6 +181,21 @@ function formatWaitSeconds(seconds: number | null | undefined): string {
|
|||||||
return `${minutes}分${secs}秒`;
|
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 {
|
function resultNumber(result: Record<string, unknown> | null | undefined, key: string): number | null {
|
||||||
const value = result?.[key];
|
const value = result?.[key];
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||||
@@ -267,7 +282,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
|
|
||||||
// 换绑时间:优先最近一次查询/拦截任务结果,回退账号落库字段
|
// 换绑时间:优先最近一次查询/拦截任务结果,回退账号落库字段
|
||||||
const latestChangeWaitByAccount = useMemo(() => {
|
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) {
|
for (const task of tasks) {
|
||||||
const taskTypes = isEsportsHandbook
|
const taskTypes = isEsportsHandbook
|
||||||
? ['prepare_esports_bind', 'get_esports_bind_qr', 'query_esports_game_name', 'confirm_esports_bind']
|
? ['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 (!taskTypes.includes(task.task_type)) continue;
|
||||||
if (!['success', 'failed'].includes(task.status)) continue;
|
if (!['success', 'failed'].includes(task.status)) continue;
|
||||||
const wait = resultNumber(task.result, 'change_role_wait_time');
|
const wait = resultNumber(task.result, 'change_role_wait_time');
|
||||||
|
const canChangeTime = resultNumber(task.result, 'can_change_time');
|
||||||
// get_bind_qr 只有冷却拦截时才有 wait;无 wait 时跳过,避免覆盖
|
// get_bind_qr 只有冷却拦截时才有 wait;无 wait 时跳过,避免覆盖
|
||||||
if (!isEsportsHandbook && task.task_type === 'get_bind_qr' && wait == null) continue;
|
if (!isEsportsHandbook && task.task_type === 'get_bind_qr' && wait == null) continue;
|
||||||
const existing = map.get(task.account_id);
|
const existing = map.get(task.account_id);
|
||||||
if (existing && existing.taskId > task.id) continue;
|
if (existing && existing.taskId > task.id) continue;
|
||||||
const text = resultText(task.result, 'change_role_wait_text') || formatWaitSeconds(wait);
|
const esportsStatus = isEsportsHandbook ? esportsRebindStatus(canChangeTime) : null;
|
||||||
map.set(task.account_id, { wait, text, taskId: task.id });
|
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;
|
return map;
|
||||||
}, [isEsportsHandbook, tasks]);
|
}, [isEsportsHandbook, tasks]);
|
||||||
@@ -610,7 +629,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
const esportsPlatName = resultText(esportsResult, 'plat_name');
|
const esportsPlatName = resultText(esportsResult, 'plat_name');
|
||||||
const esportsRoleLine = [esportsPlatName, esportsAreaName].filter(Boolean).join(' / ');
|
const esportsRoleLine = [esportsPlatName, esportsAreaName].filter(Boolean).join(' / ');
|
||||||
const esportsCanChangeRole = Boolean(esportsBindTask && !esportsConfirmRunning);
|
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 esportsQrUrl = resultText(esportsLatestQrTask?.result, 'url');
|
||||||
const esportsAccountName = esportsBindTask
|
const esportsAccountName = esportsBindTask
|
||||||
? (
|
? (
|
||||||
@@ -786,6 +806,20 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
|
title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const fromTask = latestChangeWaitByAccount.get(record.id);
|
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 ?? (
|
const wait = fromTask?.wait ?? (
|
||||||
isEsportsHandbook ? record.esports_change_role_wait_time : record.change_role_wait_time
|
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>;
|
return <Text type="secondary">{text || '-'}</Text>;
|
||||||
},
|
},
|
||||||
sorter: (a, b) => {
|
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;
|
?? (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;
|
?? (isEsportsHandbook ? b.esports_change_role_wait_time : b.change_role_wait_time) ?? -1;
|
||||||
return aw - bw;
|
return aw - bw;
|
||||||
},
|
},
|
||||||
@@ -1391,8 +1429,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
<Text type="secondary">当前未检测到游戏角色,请先切换角色。</Text>
|
<Text type="secondary">当前未检测到游戏角色,请先切换角色。</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{esportsChangeWait != null && esportsChangeWait > 0 && esportsRoleName ? (
|
{esportsRebind && esportsRoleName ? (
|
||||||
<Text type="secondary">换绑冷却剩余:{formatWaitSeconds(esportsChangeWait)}</Text>
|
<Text type="secondary">
|
||||||
|
{esportsRebind.available ? '当前可换绑' : `可换绑时间:${esportsRebind.text}`}
|
||||||
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{esportsQrUrl ? (
|
{esportsQrUrl ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user