diff --git a/docs/自动化与资料收集规划.md b/docs/自动化与资料收集规划.md index 4bfcfe0..660ea4a 100644 --- a/docs/自动化与资料收集规划.md +++ b/docs/自动化与资料收集规划.md @@ -1176,6 +1176,11 @@ OSS_PREFIX=collect P1a 踩过一次:`.env` 加了 `AUTOMATION_ENABLED=true`,容器里查不到,调度器照旧不启动; 早先 §15.1 写入的 `ORDER_SITE_*` 与 `OSS_*` 同样没生效。新增变量时三处都要动: `.env`(服务器实值)+ `docker-compose.yml`(透传)+ `.env.example`(留档); +- **注意方言差异:生产是 MySQL 8,测试是 SQLite**。SQLite 能吃下的写法 MySQL 未必认,行为测试也发现不了。 + P1a 踩过一次:`ORDER BY ck_checked_at ASC NULLS FIRST` 在 SQLite 正常,MySQL 直接报 + `1064 ... near 'NULLS FIRST'`。跨方言的排序用可移植写法(如 `col IS NULL DESC, col ASC`), + 并在测试里按 MySQL 方言 `compile()` 一次做回归。**P0 更要注意:它连的是 order_site 的 PostgreSQL**, + 三套方言(MySQL / SQLite / PG)都出现过,写 SQL 前先想清楚方言; - **DB 读出来的时间是 naive UTC**:`datetime` 列取回后不带时区,直接和 `datetime.now(UTC)` 比较会 `TypeError: can't compare offset-naive and offset-aware`(P1a 踩过一次)。比较前一律用 `_as_utc()`(`services/yyb_service.py` 与 `services/automation/scheduler.py` 各有一份)补时区。 diff --git a/tests/test_automation_ck_health.py b/tests/test_automation_ck_health.py index 45a1fff..8cc416e 100644 --- a/tests/test_automation_ck_health.py +++ b/tests/test_automation_ck_health.py @@ -130,6 +130,22 @@ class TestCkHealthSelection(_Base): assert ids[0] == never.id assert stale.id in ids + def test_generated_sql_is_mysql_compatible(self): + """回归:选行 SQL 不能出现 MySQL 不认识的 NULLS FIRST。 + + 生产是 MySQL、本地测试是 SQLite(SQLite 支持 NULLS FIRST,所以行为测试 + 发现不了),这里按 MySQL 方言编译一次断言。 + """ + from sqlalchemy.dialects import mysql + + query = ck_health.build_select_query( + self.session, self._job(), datetime.now(UTC) + ) + sql = str(query.statement.compile(dialect=mysql.dialect())).upper() + assert "NULLS FIRST" not in sql and "NULLS LAST" not in sql + # 「没检测过的优先」要用可移植写法表达 + assert "IS NULL DESC" in sql + def test_batch_size_caps_the_round(self): job = self._job() job.batch_size = 2 diff --git a/web/backend/services/automation/jobs/ck_health.py b/web/backend/services/automation/jobs/ck_health.py index 93fe1d5..fa44f9b 100644 --- a/web/backend/services/automation/jobs/ck_health.py +++ b/web/backend/services/automation/jobs/ck_health.py @@ -24,11 +24,15 @@ JOB_KEY = "ck_health" FLUSH_EVERY = 25 -def _select_tasks(db: Session, job: AutomationJob, now: datetime) -> list[LoginTask]: - """挑本轮要检测的记录。 +def build_select_query(db: Session, job: AutomationJob, now: datetime): + """构造本轮要检测记录的查询(与取行分开,便于按方言编译做回归)。 口径与人工点检测一致(``cookie_operable_filter``:持有 CK 且没有在途尝试), 再加三条:距上次检测超过作业间隔、账号允许自动管理、最久没查的优先。 + + 排序刻意不用 ``NULLS FIRST``:那是 PostgreSQL/SQLite 语法,**MySQL 会报 + 1064 语法错误**(生产踩过一次)。改用 ``ck_checked_at IS NULL DESC``, + 三个方言下语义一致且都是合法 SQL。 """ cutoff = now - timedelta(seconds=max(int(job.interval_seconds or 0), 60)) return ( @@ -42,12 +46,19 @@ def _select_tasks(db: Session, job: AutomationJob, now: datetime) -> list[LoginT LoginTask.ck_checked_at < cutoff, ) ) - .order_by(LoginTask.ck_checked_at.asc().nullsfirst(), LoginTask.id.asc()) + .order_by( + LoginTask.ck_checked_at.is_(None).desc(), + LoginTask.ck_checked_at.asc(), + LoginTask.id.asc(), + ) .limit(max(int(job.batch_size or 0), 1)) - .all() ) +def _select_tasks(db: Session, job: AutomationJob, now: datetime) -> list[LoginTask]: + return build_select_query(db, job, now).all() + + def _detect(task: LoginTask) -> dict[str, Any]: try: return check_douyu_cookie(task.cookie or "")