Files
live-hub-py/docker-compose.yml
T
yml2213 65a4590291 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 负责转本地。
2026-10-02 20:27:07 +08:00

156 lines
7.3 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
services:
douyu-login:
build:
context: .
# 生产服务只构建 runtime 阶段;test 阶段仅供本地回归测试。
target: runtime
args:
# 服务器默认走阿里云镜像;本地测试设为 false。
USE_CHINA_MIRRORS: ${USE_CHINA_MIRRORS:-true}
restart: unless-stopped
mem_limit: ${DOUYU_LOGIN_MEM_LIMIT:-2g}
cpus: ${DOUYU_LOGIN_CPUS:-2}
ports:
# 保持现有服务端口;可通过 APP_PORT 覆盖。
- "${APP_PORT:-8000}:8800"
volumes:
# 持久化运行时文件和日志,数据库由 mysql-data 卷管理。
- ./data:/app/data
- ./logs:/app/logs
environment:
- TZ=Asia/Shanghai
# 应用日志等级由 .env 控制;生产环境默认只记录 INFO 及以上。
- LOG_LEVEL=${LOG_LEVEL:-INFO}
# 统一写入宿主机挂载的 ./logs,避免容器重建后丢失或写入不可见路径。
- LOG_DIR=/app/logs
- LOG_ROTATION_SIZE=${LOG_ROTATION_SIZE:-50M}
- LOG_RETENTION_DAYS=${LOG_RETENTION_DAYS:-14}
# MySQL 仅在 Compose 内网开放,不映射宿主机端口。
- DB_HOST=mysql
- DB_PORT=3306
- DB_NAME=${MYSQL_DATABASE:-douyu_login}
- DB_USER=${MYSQL_USER:-douyu_login}
- DB_PASSWORD=${MYSQL_PASSWORD:?请在 .env 中设置 MYSQL_PASSWORD}
- DB_POOL_SIZE=${DB_POOL_SIZE:-20}
- DB_MAX_OVERFLOW=${DB_MAX_OVERFLOW:-20}
- DB_POOL_TIMEOUT=${DB_POOL_TIMEOUT:-30}
- DB_POOL_RECYCLE=${DB_POOL_RECYCLE:-1800}
# JWT 密钥(生产环境务必修改,可用 python -c "import secrets; print(secrets.token_urlsafe(32))" 生成)
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-}
# 敏感字段落盘加密密钥(生产环境务必长期保持不变)
- APP_ENCRYPTION_KEY=${APP_ENCRYPTION_KEY:-}
# 默认管理员账号密码(仅首次启动建库时生效)
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
# Cookie 安全标志(HTTPS 部署时设为 true)
- COOKIE_SECURE=${COOKIE_SECURE:-false}
# CORS 允许的源(逗号分隔)
- CORS_ORIGINS=${CORS_ORIGINS:-}
# Worker 已合并到本容器,仅通过回环地址访问。
- YYB_WORKER_URL=http://127.0.0.1:8810
- YYB_WORKER_KEY=${YYB_WORKER_KEY:-}
# 鱼翅直充供应商凭据仅通过容器环境变量注入,不入库、不返回前端。
- FISH_FIN_RECHARGE_BASE_URL=${FISH_FIN_RECHARGE_BASE_URL:-}
- FISH_FIN_RECHARGE_APP_ID=${FISH_FIN_RECHARGE_APP_ID:-}
- FISH_FIN_RECHARGE_APP_KEY=${FISH_FIN_RECHARGE_APP_KEY:-}
- FISH_FIN_RECHARGE_APP_SECRET=${FISH_FIN_RECHARGE_APP_SECRET:-}
- FISH_FIN_RECHARGE_NOTIFY_URL=${FISH_FIN_RECHARGE_NOTIFY_URL:-}
- FISH_FIN_RECHARGE_TIMEOUT=${FISH_FIN_RECHARGE_TIMEOUT:-20}
- FISH_FIN_RECHARGE_DEBUG=${FISH_FIN_RECHARGE_DEBUG:-false}
# Roundcube 邮件验证码读取服务地址(不填则使用代码默认值)
- MAIL_ROUNDCUBE_URL=${MAIL_ROUNDCUBE_URL:-}
# 生产容器禁止 reload;开发环境由 dev.sh 单独启动 reload。
- UVICORN_RELOAD=false
# 自动化调度(见 docs/自动化与资料收集规划.md §7)。
# 这个开关只决定「是否起调度线程」;各作业还有自己的开关,默认关闭,
# 在 /automation 页面打开。
- AUTOMATION_ENABLED=${AUTOMATION_ENABLED:-false}
# order_site 订单只读库(P0 订单读取适配层用;未配置时适配层整体降级)
- ORDER_SITE_DATABASE_URL=${ORDER_SITE_DATABASE_URL:-}
- ORDER_SITE_LOOKUP_ENABLED=${ORDER_SITE_LOOKUP_ENABLED:-false}
- ORDER_SITE_LOOKUP_TIMEOUT_SECONDS=${ORDER_SITE_LOOKUP_TIMEOUT_SECONDS:-3}
# 用户上传图片的 OSS(P2 资料收集用)
- OSS_ENDPOINT=${OSS_ENDPOINT:-}
- OSS_BUCKET=${OSS_BUCKET:-}
- OSS_ACCESS_KEY_ID=${OSS_ACCESS_KEY_ID:-}
- OSS_ACCESS_KEY_SECRET=${OSS_ACCESS_KEY_SECRET:-}
- OSS_PREFIX=${OSS_PREFIX:-collect}
networks:
- default
- order-site-net
depends_on:
mysql:
condition: service_healthy
# 增加文件描述符限制,避免 "Too many open files" 错误
ulimits:
nofile:
soft: 65536
hard: 65536
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8800/api/health'); urllib.request.urlopen('http://127.0.0.1:8810/health')"]
interval: 30s
timeout: 5s
retries: 3
# 回归测试容器:docker compose --profile test run --rm test
# 镜像内为构建时的代码,改动代码后需重新构建(仅按需启动,不影响 docker compose up)。
test:
build:
context: .
target: test
args:
USE_CHINA_MIRRORS: ${USE_CHINA_MIRRORS:-true}
profiles: ["test"]
entrypoint: ["python", "-m", "pytest"]
mysql:
image: ${MYSQL_IMAGE:-docker.m.daocloud.io/library/mysql:8.4}
restart: unless-stopped
mem_limit: ${MYSQL_MEM_LIMIT:-2g}
cpus: ${MYSQL_CPUS:-2}
ports:
# 仅绑定宿主机本地地址,供 ./dev.sh 的本地后端连接,不对外网开放。
- "${MYSQL_BIND_HOST:-127.0.0.1}:${MYSQL_HOST_PORT:-3307}:3306"
environment:
- TZ=Asia/Shanghai
- MYSQL_DATABASE=${MYSQL_DATABASE:-douyu_login}
- MYSQL_USER=${MYSQL_USER:-douyu_login}
- MYSQL_PASSWORD=${MYSQL_PASSWORD:?请在 .env 中设置 MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:?请在 .env 中设置 MYSQL_ROOT_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql
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。
- --slow-query-log=${MYSQL_SLOW_QUERY_LOG:-1}
- --long-query-time=${MYSQL_LONG_QUERY_TIME:-1}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 10s
timeout: 5s
retries: 12
start_period: 20s
volumes:
mysql-data:
# 复用项目改名前的旧数据卷 douyu_login_py_mysql-data(4G+ 真实数据)。
# 注意:曾用默认值 live-hub-py_mysql-data,那是**另一个空卷** —— 目录改名后第一次
# 重建容器会挂到空卷上,看起来像数据全丢(2026-10-02 实际发生过)。
# 因此默认值与 external 都指向真实卷,不依赖 .env 是否设置。
name: ${MYSQL_VOLUME_NAME:-douyu_login_py_mysql-data}
external: ${MYSQL_VOLUME_EXTERNAL:-true}
networks:
order-site-net:
external: true
name: order_site_default