协作台账:新表 huya_external_orders 替代 Excel 多人协同表(设计见
docs/设计-虎牙外部CK协作台账.md),一条 CK 一行,所有人共享、改动留痕。
- 前端 pages/HuyaCollabPage.tsx + 菜单「虎牙 → 外部单协作」/huya/collab:
内联编辑(带乐观锁 version)、图片缩略图、详情抽屉(字段表单 + 两个图片槽位 +
操作时间线)、粘贴导入弹窗(带 TSV 预览)、批量/单条验证 CK、页头统计条、筛选
- 两套状态机:业务 pending→bound→recharged→scripted→done(旁路 void)、
凭据 unverified/valid/invalid/disabled;唯一写入口在 services/huya_collab_service.py,
字段级审计 + 整数 version 乐观锁(不用 updated_at:DATETIME 只到秒)
- CK 只读探针 check_user_bind_game_account(真机判据:响应非 None 且 status == 200),
成功即覆盖写绑定快照 bind_game_account/bind_game_role/bind_game_channel(迁移 0056),
失败只改凭据状态、不动业务状态;人工只能停用/恢复(POST /{id}/ck/status)
- 贴/换/重建 CK、TSV 批量导入(SAVEPOINT 单行隔离,部分失败不整体失败)、
权限 huya:collab:view/edit/manage(删除只给运营)
- 孤儿图清理脚本 scripts/cleanup_orphan_file_assets.py(默认 --dry-run)
图片上传(协作台账截图用):OSS/本地双后端 + 缩略图变体 + /api/files 代理回读,
权限 file:upload / file:view。
验证:pytest 880 passed / 6 skipped;ruff format 全绿、ruff check 仅剩既有告警;
前端 tsc -b / eslint / vite build 通过;接口端到端冒烟 29 项全绿(TestClient,探针用假客户端);
探针真机冒烟 3 例(有效 CK=True、别人 uid=False、裸 cookie=False)。
159 lines
7.5 KiB
YAML
159 lines
7.5 KiB
YAML
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}
|
||
# 用户上传图片的桶(在线协作 / P2 资料收集共用;生产用内网 endpoint)。
|
||
# OSS_MODE=oss 才走对象存储,否则落容器内 ./data/file_storage。
|
||
- OSS_MODE=${OSS_MODE:-local}
|
||
- 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}
|
||
- OSS_MAX_UPLOAD_SIZE_MB=${OSS_MAX_UPLOAD_SIZE_MB:-10}
|
||
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
|