feat(login): 任务列表支持分页、跨页全选与一键清空

任务量早就过了单页能装下的规模(库里 1.4 万条,单批就 177 条),但接口
硬编码 limit(200)、前端 pagination={false},于是「全选」其实只能选到前
200 条,失败任务清不干净。

- 接口加 page/page_size/status:不传 page 时仍返回裸数组(Cookie 页等
  老调用方依赖这个契约,不能改);传了才返回 {items,total},total 供跨页全选。
- 删除支持 all_matching:按当前筛选条件删全部,不必先翻完所有页去勾。
- 前端:分页控件(20/50/100/200/500,记住选择)、状态筛选、表头
  「选择全部 N 条(含其他页)」、「清空失败」一键操作。

两条护栏:
- **pending/running 一律不删** —— 删掉会让批次线程找不到记录;要停批次
  走 /stop/{batch_id}。
- 不给任何条件就拒绝删除,绝不默认全删。

顺带:带 join 的 query 不能 bulk delete(SQLAlchemy 会拒绝),改成先查
id 再按主键删。
This commit is contained in:
yml2213
2026-10-03 10:37:52 +08:00
parent 5995f16c39
commit a0db26def3
5 changed files with 550 additions and 34 deletions
+33
View File
@@ -52,6 +52,39 @@ live-hub-py/
./deploy.sh reset # 重置所有数据(需二次确认)
```
## 时区口径(查库前必读)
**全库时间列一律存 UTC 裸值**(`DATETIME`,无 `TIMESTAMP` 列、无 `CURRENT_TIMESTAMP` 默认值)。MySQL 容器以
`--default-time-zone=+00:00` 启动,因此 **`NOW()` 返回的也是 UTC** —— 这**不是故障**,是刻意对齐存储口径。
| 层 | 看到的时间 | 说明 |
| --- | --- | --- |
| 数据库裸值 / `NOW()` | UTC | 命令行直连就是这个 |
| ORM 读出(`UTCDateTime`) | `+00:00` aware | 读出来永远带时区,比较不会踩 naive/aware 的坑 |
| HTTP 接口返回 | `...+00:00` | 前端按本地时区渲染 |
| 前端显示 | 北京时间(UTC+8) | `dayjs` 自动转换 |
| 应用日志 | 北京时间 | 容器内 `TZ=Asia/Shanghai` |
> ⚠️ **别拿 `SELECT NOW()` 当"真实时间"去和业务时间比对** —— 它比你手表慢 8 小时是正常的。
> 已踩过一次:据此误判"数据库时区错了",实际端到端完全正确。
命令行查库要按北京时间看,用 `CONVERT_TZ`:
```sql
SELECT id, status,
CONVERT_TZ(created_at, '+00:00', '+08:00') AS 创建时间,
CONVERT_TZ(finished_at, '+00:00', '+08:00') AS 完成时间
FROM login_tasks ORDER BY id DESC LIMIT 10;
```
按"北京时间某段"筛选用同样的换算,别直接写本地时间字面量:
```sql
-- 查北京时间 10-03 09:00 之后的任务
SELECT * FROM login_tasks
WHERE created_at >= CONVERT_TZ('2026-10-03 09:00:00', '+08:00', '+00:00');
```
## 运行时与密钥
生产是 **单个应用容器 + 内网 MySQL 8.4**。数据库不映射公网;本机 `./dev.sh` 通过 `127.0.0.1:3307` 连 compose 里的 MySQL。