refactor(time): 时间列统一走 UTCDateTime,并把 MySQL 会话时区对齐成 UTC
问题不是「UTC 还是本地时间」,而是这套口径有两个半吊子的地方: 1. 库里存 naive UTC、读出来也是裸时间,于是每一处比较/输出都得自己补时区。 项目里因此散了三份 _as_utc 和一堆 replace(tzinfo=UTC),P1a 排下一轮时间 时还炸过一次 TypeError: can't compare offset-naive and offset-aware。 2. MySQL 会话时区跟随系统 +08:00,而库里存的是 UTC —— SQL 里的 NOW()/ CURDATE() 比库里的时间快 8 小时,查询会静默返回空(P1a 排查时就遇到过, 看着像「没有数据」)。 改动: - 新增 database.UTCDateTime:写时 aware→转 UTC 去 tzinfo、naive 视为 UTC; 读时一律补 UTC 时区。底层 DDL 仍是 DATETIME,**不需要迁移**。models.py 的 28 个时间列全部改用它,模型上的时间字段从此不再是裸值; - docker-compose 给 mysql 加 --default-time-zone=+00:00,让 SQL 时间函数与存储 口径一致。安全性已核实:全库 0 个 TIMESTAMP 列、0 个 CURRENT_TIMESTAMP 默认值,改这里不会改变任何已有数据的语义; - 新增 tests/test_utc_datetime.py:读出必须 aware、能直接和 datetime.now(UTC) 比较、带 +08:00 的写入要落成 UTC、naive 写入视为 UTC,并有一条元测试扫描 Base.metadata,防止以后新增时间列忘了用 UTCDateTime。 保留既有的 _as_utc 补丁(对 aware 值是幂等的,属防御性代码);新代码不再需要。 「存 UTC、算 UTC、显示本地」三条线至此各自明确:存储与比较统一 UTC,日志按 容器 TZ 显示本地时间,前端 formatTime 负责转本地。
This commit is contained in:
@@ -122,6 +122,12 @@ services:
|
||||
command:
|
||||
- --character-set-server=utf8mb4
|
||||
- --collation-server=utf8mb4_0900_ai_ci
|
||||
# 会话时区与存储口径对齐:库里所有时间列存的都是 UTC(应用一律写
|
||||
# datetime.now(UTC),MySQL 绑定参数丢掉 tzinfo 后落库即 UTC 裸值)。
|
||||
# 若仍跟随系统 +08:00,SQL 里的 NOW()/CURDATE() 会比库里时间快 8 小时,
|
||||
# 查询静默返回空、看着像「没数据」。全库无 TIMESTAMP 列、也没有
|
||||
# CURRENT_TIMESTAMP 默认值,因此改这里不会改变任何已有数据的语义。
|
||||
- --default-time-zone=+00:00
|
||||
# 业务表已超过 300MiB;可通过 .env 调整,默认给 MySQL 512MiB 缓冲池。
|
||||
- --innodb-buffer-pool-size=${MYSQL_INNODB_BUFFER_POOL_SIZE:-512M}
|
||||
# 默认记录 1 秒以上查询,便于验证查询优化;稳定后可设 MYSQL_SLOW_QUERY_LOG=0。
|
||||
|
||||
@@ -156,4 +156,4 @@ def test_solver_sms_flow():
|
||||
mock_submit.assert_called_once()
|
||||
verify_url, verify_payload = mock_submit.call_args[0]
|
||||
assert "verify-sms-code" in verify_url
|
||||
assert verify_payload["data"]["smsCode"] == "654321".encode().hex()
|
||||
assert verify_payload["data"]["smsCode"] == b"654321".hex()
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""回归:时间列统一走 ``UTCDateTime`` —— 读出来必须带时区,写进去必须存 UTC。
|
||||
|
||||
背景:库里存的是 naive UTC,读出来也是裸时间,于是每一处比较/输出都要自己记得
|
||||
补时区。项目里因此散了三份 ``_as_utc`` 和一堆 ``replace(tzinfo=UTC)``,还踩过一次
|
||||
|
||||
TypeError: can't compare offset-naive and offset-aware datetimes
|
||||
|
||||
(P1a 的 CK 巡检排下一轮时间时炸的)。现在把边界收在 ``UTCDateTime``:
|
||||
写时 aware→UTC 裸值、naive 视为 UTC;读时一律补 UTC 时区。
|
||||
|
||||
这组测试盯三件事:读出来是 aware、能和 ``datetime.now(UTC)`` 直接比、写进去
|
||||
落的是 UTC 而不是本地时间。
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta, timezone
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from web.backend.database import Base, UTCDateTime
|
||||
from web.backend.models import AutomationRun, LoginTask
|
||||
|
||||
|
||||
class TestUtcDateTime:
|
||||
def setup_method(self):
|
||||
self.engine = create_engine("sqlite://")
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.session = sessionmaker(bind=self.engine)()
|
||||
|
||||
def teardown_method(self):
|
||||
self.session.close()
|
||||
|
||||
def _row(self, started_at):
|
||||
self.session.add(
|
||||
AutomationRun(job_key="k", started_at=started_at, status="success")
|
||||
)
|
||||
self.session.commit()
|
||||
return self.session.query(AutomationRun).one()
|
||||
|
||||
def test_read_back_is_timezone_aware(self):
|
||||
row = self._row(datetime(2026, 10, 2, 12, 0, tzinfo=UTC))
|
||||
assert row.started_at.tzinfo is not None
|
||||
assert row.started_at.utcoffset() == timedelta(0)
|
||||
|
||||
def test_read_back_value_can_compare_with_now(self):
|
||||
"""这条就是当初炸掉的写法:库里的时间直接和 datetime.now(UTC) 比。"""
|
||||
row = self._row(datetime.now(UTC) - timedelta(minutes=1))
|
||||
assert row.started_at < datetime.now(UTC) # 不再 TypeError
|
||||
|
||||
def test_write_converts_local_aware_to_utc(self):
|
||||
"""带 +08:00 的时间要按 UTC 落库,不能把本地壁钟直接存进去。"""
|
||||
local = datetime(2026, 10, 2, 20, 0, tzinfo=timezone(timedelta(hours=8)))
|
||||
row = self._row(local)
|
||||
assert row.started_at == datetime(2026, 10, 2, 12, 0, tzinfo=UTC)
|
||||
raw = self.session.execute(
|
||||
text("SELECT started_at FROM automation_runs")
|
||||
).scalar()
|
||||
assert str(raw).startswith("2026-10-02 12:00")
|
||||
|
||||
def test_naive_write_is_treated_as_utc(self):
|
||||
"""库里读出来的裸值再写回去(常见于搬迁/复制)不能被当成本地时间再减 8 小时。"""
|
||||
naive_utc = datetime(2026, 10, 2, 12, 0) # noqa: DTZ001 刻意构造裸 UTC
|
||||
row = self._row(naive_utc)
|
||||
assert row.started_at == datetime(2026, 10, 2, 12, 0, tzinfo=UTC)
|
||||
|
||||
def test_none_stays_none(self):
|
||||
assert UTCDateTime().process_bind_param(None, None) is None
|
||||
assert UTCDateTime().process_result_value(None, None) is None
|
||||
|
||||
def test_all_model_datetime_columns_use_utc_type(self):
|
||||
"""新增时间列若忘了用 UTCDateTime,这里直接失败。"""
|
||||
offenders = []
|
||||
for table in Base.metadata.sorted_tables:
|
||||
for column in table.columns:
|
||||
if isinstance(column.type, UTCDateTime):
|
||||
continue
|
||||
if str(column.type).upper().startswith("DATETIME"):
|
||||
offenders.append(f"{table.name}.{column.name}")
|
||||
assert not offenders, f"这些时间列没用 UTCDateTime: {offenders}"
|
||||
|
||||
def test_login_task_attempt_at_round_trips_as_utc(self):
|
||||
"""真实模型抽查:写 aware、读 aware、能直接比较。"""
|
||||
task = LoginTask(
|
||||
batch_id="b",
|
||||
account_id=1,
|
||||
created_by=1,
|
||||
attempt_at=datetime(2026, 10, 2, 12, 0, tzinfo=UTC),
|
||||
)
|
||||
self.session.add(task)
|
||||
self.session.commit()
|
||||
self.session.refresh(task)
|
||||
assert task.attempt_at == datetime(2026, 10, 2, 12, 0, tzinfo=UTC)
|
||||
# 模型侧保证 aware,所以业务代码可以直接比较
|
||||
assert task.attempt_at < datetime.now(UTC)
|
||||
+38
-1
@@ -1,11 +1,13 @@
|
||||
"""数据库引擎与会话管理"""
|
||||
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy import DateTime, create_engine, event
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
@@ -86,6 +88,41 @@ SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class UTCDateTime(TypeDecorator):
|
||||
"""时间列统一走这里:**库里存 naive UTC,Python 侧永远是 tz-aware UTC**。
|
||||
|
||||
之前是全靠约定:写入用 ``datetime.now(UTC)``(MySQL 绑定参数会丢掉 tzinfo,
|
||||
落库成 UTC 裸值),读出是裸值,谁要比较或输出就得自己记得补时区 —— 项目里
|
||||
因此散了三份 ``_as_utc`` 和一堆 ``replace(tzinfo=UTC)``,还踩过一次
|
||||
``TypeError: can't compare offset-naive and offset-aware datetimes``。
|
||||
|
||||
这个装饰器把边界收在一处:
|
||||
|
||||
- 写:aware → 转 UTC 后去掉 tzinfo;naive → 视为已是 UTC,原样存;
|
||||
- 读:一律补上 UTC 时区。
|
||||
|
||||
于是模型上的时间字段永远不会是裸时间,比较、相减、序列化都不必再各自处理。
|
||||
底层 DDL 仍是 DATETIME(``impl`` 不变),**不需要迁移**。
|
||||
"""
|
||||
|
||||
impl = DateTime
|
||||
cache_ok = True
|
||||
|
||||
def process_bind_param(self, value: datetime | None, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
if value.tzinfo is None:
|
||||
return value
|
||||
return value.astimezone(UTC).replace(tzinfo=None)
|
||||
|
||||
def process_result_value(self, value: datetime | None, dialect):
|
||||
if value is None:
|
||||
return None
|
||||
if value.tzinfo is None:
|
||||
return value.replace(tzinfo=UTC)
|
||||
return value.astimezone(UTC)
|
||||
|
||||
|
||||
def get_db():
|
||||
"""FastAPI 依赖:提供数据库会话,请求结束自动关闭。"""
|
||||
db = SessionLocal()
|
||||
|
||||
+49
-46
@@ -7,7 +7,6 @@ from datetime import UTC, datetime
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
@@ -17,7 +16,7 @@ from sqlalchemy import (
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .crypto_storage import EncryptedText
|
||||
from .database import Base
|
||||
from .database import Base, UTCDateTime
|
||||
from .huya_defaults import (
|
||||
HUYA_DEFAULT_BIND_ACT_ID,
|
||||
HUYA_DEFAULT_OUTER_ACT_ID,
|
||||
@@ -50,12 +49,12 @@ class User(Base):
|
||||
custom_permissions: Mapped[list[str] | None] = mapped_column(
|
||||
JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, nullable=True, index=True
|
||||
UTCDateTime, nullable=True, index=True
|
||||
)
|
||||
deleted_username: Mapped[str] = mapped_column(String(64), default="")
|
||||
|
||||
@@ -121,9 +120,9 @@ class Account(Base):
|
||||
xpd_balance: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
xpd_fragments: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
xpd_bind_status: Mapped[str] = mapped_column(String(32), default="")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
assigned_user: Mapped[User | None] = relationship(
|
||||
@@ -153,8 +152,8 @@ class LoginTask(Base):
|
||||
cookie: Mapped[str] = mapped_column(EncryptedText(), default="")
|
||||
message: Mapped[str] = mapped_column(String(512), default="")
|
||||
created_by: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
# Cookie 有效性检测结果(持久化,刷新不丢失)
|
||||
ck_check_status: Mapped[str] = mapped_column(
|
||||
String(16), default=""
|
||||
@@ -162,7 +161,7 @@ class LoginTask(Base):
|
||||
ck_check_result: Mapped[dict | None] = mapped_column(
|
||||
JSON, nullable=True
|
||||
) # {fish_ball, nickname, level, message}
|
||||
ck_checked_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
ck_checked_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
# 最近一次登录尝试的状态。批次只写 attempt_*,只有登录成功才会把新 CK
|
||||
# 写回 status/cookie/message,因此停止、失败、进程重启都不会破坏已有 CK。
|
||||
attempt_status: Mapped[str] = mapped_column(
|
||||
@@ -174,12 +173,12 @@ class LoginTask(Base):
|
||||
attempt_batch_id: Mapped[str] = mapped_column(
|
||||
String(64), default="", server_default=""
|
||||
)
|
||||
attempt_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
attempt_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
# 自动重登(P1b)用的退避状态。P1a 的巡检不碰这几个字段。
|
||||
relogin_attempts: Mapped[int] = mapped_column(
|
||||
Integer, default=0, server_default="0"
|
||||
) # 连续重登尝试次数,成功即清零
|
||||
relogin_next_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
relogin_next_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
relogin_last_error: Mapped[str] = mapped_column(
|
||||
String(512), default="", server_default=""
|
||||
)
|
||||
@@ -208,8 +207,8 @@ class DouyuTask(Base):
|
||||
message: Mapped[str] = mapped_column(String(512), default="")
|
||||
result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
created_by: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
|
||||
account: Mapped[Account] = relationship("Account", back_populates="douyu_tasks")
|
||||
|
||||
@@ -227,7 +226,7 @@ class DouyuWorkbenchAccount(Base):
|
||||
account_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("accounts.id"), nullable=False, index=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
@@ -247,7 +246,7 @@ class DouyuWorkbench(Base):
|
||||
)
|
||||
handbook_scope: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
@@ -288,15 +287,17 @@ class YybRechargeTask(Base):
|
||||
result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
login_qr_data: Mapped[str] = mapped_column(EncryptedText(), default="")
|
||||
payment_qr_data: Mapped[str] = mapped_column(EncryptedText(), default="")
|
||||
payment_started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
payment_started_at: Mapped[datetime | None] = mapped_column(
|
||||
UTCDateTime, nullable=True
|
||||
)
|
||||
payment_qr_created_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
UTCDateTime, nullable=True
|
||||
)
|
||||
payment_last_checked_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
UTCDateTime, nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
|
||||
|
||||
class DouyuConfig(Base):
|
||||
@@ -331,7 +332,7 @@ class DouyuConfig(Base):
|
||||
gift_id: Mapped[str] = mapped_column(String(64), default="23643")
|
||||
skin_id: Mapped[str] = mapped_column(String(64), default="2942")
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -347,7 +348,7 @@ class DouyuGoodsSnapshot(Base):
|
||||
status: Mapped[str] = mapped_column(String(32), default="")
|
||||
raw: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -365,7 +366,7 @@ class DouyuXpdGoodsSnapshot(Base):
|
||||
goods_left: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
raw: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -381,7 +382,7 @@ class DouyuEsportsGoodsSnapshot(Base):
|
||||
status: Mapped[str] = mapped_column(String(32), default="")
|
||||
raw: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -414,9 +415,9 @@ class HuyaAccount(Base):
|
||||
assigned_to: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=True, index=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
assigned_user: Mapped[User | None] = relationship(
|
||||
@@ -450,9 +451,9 @@ class HuyaRegisterBatch(Base):
|
||||
success_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
failed_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
stopped_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
started_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
|
||||
items: Mapped[list[HuyaRegisterItem]] = relationship(
|
||||
"HuyaRegisterItem", back_populates="batch", cascade="all, delete-orphan"
|
||||
@@ -487,8 +488,8 @@ class HuyaRegisterItem(Base):
|
||||
password: Mapped[str] = mapped_column(EncryptedText(), default="")
|
||||
password_changed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
cookie: Mapped[str] = mapped_column(EncryptedText(), default="")
|
||||
started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
started_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
|
||||
batch: Mapped[HuyaRegisterBatch] = relationship(
|
||||
"HuyaRegisterBatch", back_populates="items"
|
||||
@@ -516,7 +517,9 @@ class HuyaRegisterSuccessLog(Base):
|
||||
created_by: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
UTCDateTime, default=_utcnow, index=True
|
||||
)
|
||||
|
||||
|
||||
class HuyaTask(Base):
|
||||
@@ -538,8 +541,8 @@ class HuyaTask(Base):
|
||||
message: Mapped[str] = mapped_column(String(512), default="")
|
||||
result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
created_by: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
|
||||
account: Mapped[HuyaAccount] = relationship("HuyaAccount", back_populates="tasks")
|
||||
|
||||
@@ -557,7 +560,7 @@ class HuyaWorkbenchAccount(Base):
|
||||
account_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("huya_accounts.id"), nullable=False, index=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
@@ -580,7 +583,7 @@ class HuyaWorkbench(Base):
|
||||
)
|
||||
handbook_scope: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
@@ -606,7 +609,7 @@ class HuyaConfig(Base):
|
||||
String(16), default=HUYA_DEFAULT_PAY_CHANNEL
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -624,7 +627,7 @@ class HuyaGoodsSnapshot(Base):
|
||||
remain_text: Mapped[str] = mapped_column(String(64), default="")
|
||||
raw: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -648,7 +651,7 @@ class HuyaRechargeGoodsSnapshot(Base):
|
||||
task_name: Mapped[str] = mapped_column(String(256), default="")
|
||||
raw: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -684,7 +687,7 @@ class AuditLog(Base):
|
||||
detail: Mapped[str] = mapped_column(Text, default="")
|
||||
# 旧记录未标记结果时为 NULL,避免把历史操作误判成失败。
|
||||
success: Mapped[bool | None] = mapped_column(Boolean, nullable=True, index=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
|
||||
|
||||
class AutomationJob(Base):
|
||||
@@ -702,14 +705,14 @@ class AutomationJob(Base):
|
||||
batch_size: Mapped[int] = mapped_column(Integer, default=200, server_default="200")
|
||||
concurrency: Mapped[int] = mapped_column(Integer, default=5, server_default="5")
|
||||
config: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
last_run_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
next_run_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
last_run_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
next_run_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
last_status: Mapped[str] = mapped_column(String(16), default="", server_default="")
|
||||
last_message: Mapped[str] = mapped_column(
|
||||
String(512), default="", server_default=""
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=_utcnow, onupdate=_utcnow
|
||||
UTCDateTime, default=_utcnow, onupdate=_utcnow
|
||||
)
|
||||
|
||||
|
||||
@@ -720,8 +723,8 @@ class AutomationRun(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
job_key: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
started_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
finished_at: Mapped[datetime | None] = mapped_column(UTCDateTime, nullable=True)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(16), default="running", server_default="running"
|
||||
) # running / success / failed / skipped
|
||||
|
||||
Reference in New Issue
Block a user