别再手动上线了:一条命令带备份、健康检查和自动回滚

别再手动上线了:一条命令带备份、健康检查和自动回滚

.env 该有多少配置、Docker 起不来怎么用原生进程兜底、日志怎么做到"出问题时一个 ID 定位"。
「AI Agent 工程化实战」系列 · 11 项目源码:Ticnix/weather-travel-recommend-system 基于气象大数据的出行推荐系统,AI Agent 全栈项目。FastAPI + PostgreSQL(TimescaleDB/pgvector/PostGIS) + Redis; LangGraph + MCP + Skill + RAG,对接 DeepSeek API;React/Vue 前后端分离,实现 3D 天气可视化、智能出行穿搭推荐。 (项目仍在更新中)


TL;DR

  1. 配置只有一个 .env,但必须守住三条线 :Key 不进仓库、未配 Key 不阻断启动、容器内的地址归 compose 管。项目用 .env(开发)+ .env.prod.example(模板)+ compose 的 environment 覆盖(容器网络地址)三段式解决。
  2. "上线"应该是一条命令 :备份 → 停应用 → 拉镜像 → 启动 → 健康检查 → 失败自动回滚。回滚不需要重新拉镜像------旧版本镜像就在本地,所以是秒级的。
  3. 日志要能"用一个 ID 串起一次请求"contextvars 生成 request_id、贯穿所有 service 层日志、输出 JSON 单行、指标按路由模板聚合(不是真实 path)。

目录

  • 一、先看那条"一条命令上线"的脚本
  • [二、配置管理:一个 .env 里的三条线](#二、配置管理:一个 .env 里的三条线 "#%E4%BA%8C%E9%85%8D%E7%BD%AE%E7%AE%A1%E7%90%86%E4%B8%80%E4%B8%AA-env-%E9%87%8C%E7%9A%84%E4%B8%89%E6%9D%A1%E7%BA%BF")
  • [三、Docker 起不来怎么办:原生进程兜底](#三、Docker 起不来怎么办:原生进程兜底 "#%E4%B8%89docker-%E8%B5%B7%E4%B8%8D%E6%9D%A5%E6%80%8E%E4%B9%88%E5%8A%9E%E5%8E%9F%E7%94%9F%E8%BF%9B%E7%A8%8B%E5%85%9C%E5%BA%95")
  • [四、日志:一个 request_id 串起一次请求](#四、日志:一个 request_id 串起一次请求 "#%E5%9B%9B%E6%97%A5%E5%BF%97%E4%B8%80%E4%B8%AA-request_id-%E4%B8%B2%E8%B5%B7%E4%B8%80%E6%AC%A1%E8%AF%B7%E6%B1%82")
  • 五、CI:坏代码进不了主干
  • 六、小结与下一篇

一、先看那条"一条命令上线"的脚本

deploy.sh 的头部注释把设计意图写得很清楚,我直接引用:

bash 复制代码
# deploy.sh ------ 一条命令完成上线:
#   备份数据库 → 停应用 → 拉取镜像 → 启动 → 健康检查 → 失败自动回滚
#
# 用法:
#   ./deploy.sh                             # 部署主干最新版(latest)
#   IMAGE_TAG=sha-4c3abe9 ./deploy.sh       # 部署指定版本
#   IMAGE_TAG=sha-1234567 ./deploy.sh       # 同一条命令就是回滚:换个旧 tag 再跑
#
# 退出码:
#   0  部署成功
#   1  部署失败(已自动回滚到上一版本)
#   2  部署失败且回滚也失败 ------ 需要人工介入,此时数据库已有部署前备份

注意"同一条命令就是回滚" ------ 这是个很聪明的设计:不写单独的 rollback 脚本,而是把"部署哪个版本"变成参数。少一个脚本,少一处可能忘记维护的逻辑。

三个值得抄的决策

① 备份放在"停应用之后、换镜像之前"。

bash 复制代码
# 设计说明:
#   - 备份放在「停应用之后、换镜像之前」:应用停止后数据不再变化,
#     pg_dump 拿到的是一致性快照;停机窗口本来就等于部署窗口,不额外损失

"应用停止后数据不再变化" ------ 这句是备份正确性的前提。如果边写边 dump,拿到的快照可能跨在两次事务中间。

② 备份要校验"真的能用",不只检查文件存在。

bash 复制代码
# 校验备份真的可用:非空 + gzip 完整性。防止「备份了个寂寞」后继续往下走
[ -s "$BACKUP_FILE" ] || fail "备份文件为空,中止部署"
gzip -t "$BACKUP_FILE" || fail "备份文件损坏(gzip 校验未通过),中止部署"

"防止『备份了个寂寞』" ------ 磁盘满或 pg_dump 中途挂了,会留下一个 0 字节或截断的文件。不校验的话,你会在最需要它的那一刻才发现它没用。

③ 回滚不重新 pull。

bash 复制代码
#   - 回滚不重新 pull:旧版本的镜像在本地一定存在(刚才就是它跑着的),
#     所以回滚不需要网络,秒级完成
#   - 数据库/Redis 容器全程不重启,业务数据卷不受影响

整个流程是这样:

flowchart TB S[&#34;开始部署&#34;] --> A[&#34;记录当前版本<br/>.last_deployed_tag&#34;] A --> B[&#34;停应用容器<br/>postgres/redis 不动&#34;] B --> C[&#34;pg_dump 备份<br/>+ 校验 + 清理过期&#34;] C --> D{&#34;备份可用吗&#34;} D -->|&#34;否&#34;| X1[&#34;中止部署<br/>数据未受影响&#34;] D -->|&#34;是&#34;| E[&#34;拉取新镜像&#34;] E --> F[&#34;启动新版本&#34;] F --> G{&#34;健康检查<br/>三个入口&#34;} G -->|&#34;通过&#34;| Y1[&#34;写入版本记录<br/>退出码 0&#34;] G -->|&#34;失败&#34;| H{&#34;有旧版本吗&#34;} H -->|&#34;无(首次部署)&#34;| X2[&#34;如实报告<br/>退出码 2&#34;] H -->|&#34;有&#34;| I[&#34;用旧 tag 重启<br/>不重新拉取&#34;] I --> J{&#34;再次健康检查&#34;} J -->|&#34;通过&#34;| Y2[&#34;已回滚·退出码 1&#34;] J -->|&#34;失败&#34;| X2 style Y1 fill:#E1F5EE,stroke:#0F6E56,color:#04342C style Y2 fill:#FAEEDA,stroke:#854F0B,color:#633806 style X1 fill:#FCEBEB,stroke:#A32D2D,color:#501313 style X2 fill:#FCEBEB,stroke:#A32D2D,color:#501313 style G fill:#E6F1FB,stroke:#185FA5,color:#0C447C style J fill:#E6F1FB,stroke:#185FA5,color:#0C447C style D fill:#E6F1FB,stroke:#185FA5,color:#0C447C style H fill:#E6F1FB,stroke:#185FA5,color:#0C447C

一个小功能:让回滚流程可以被演练

健康检查函数有个"强制失败"开关:

bash 复制代码
# 参数 $1:是否强制判定失败("1"=演练用)。注意只有「新版本」的检查受它影响,
# 回滚后的检查永远真实探测------否则演练时无法验证"回滚后服务真的恢复了"。
check_health() {
  local force_fail="${1:-0}"
  ...
  if [ "$force_fail" != "1" ] && curl -fsS ... ; then

用法是 FORCE_HEALTHCHECK_FAIL=1 ./deploy.sh

注意括号里那句 :强制失败只作用于"新版本"的检查,回滚后的检查永远真实探测。否则演练就只能演练到"我以为回滚成功了"------这半句才是这个功能真正的价值。

而且首次部署失败时,脚本不假装回滚

bash 复制代码
if [ "$OLD_TAG" = "未知" ]; then
  # 首次部署没有"上一版本"可回滚------如实报告,不假装恢复
  printf '[deploy][错误] 这是首次部署且健康检查失败,没有旧版本可回滚。\n' >&2
  ...
  exit 2
fi

"如实报告,不假装恢复" ------ 退出码 2 的含义就是"需要人工介入",跟退出码 1(已自动恢复)区分开。


二、配置管理:一个 .env 里的三条线

项目的环境变量分三处,各自职责明确:

文件 职责 是否入库
backend/.env 本地开发全部配置 ❌ 被 .gitignore 忽略
backend/.env.prod.example 生产模板(只列会变的敏感项 ✅ 入库
docker-compose.ymlenvironment 容器网络地址覆盖 ✅ 入库

第一条线:Key 不进仓库

.gitignore 里对应的规则很直接:

gitignore 复制代码
.venv/
.env
# 各环境配置(.env.prod / .env.dev 等):敏感值一律不入库,参考 *.example
.env.*
*.log

注释里那句"参考 *.example"是关键的配套动作:光忽略不够,还得给一个模板,否则新人不知道该配哪些项。

模板文件自己就说了这件事:

bash 复制代码
# ============================================================
# 生产环境配置示例
#
# 使用方式:
#   1. 复制本文件为 backend/.env.prod,填入真实值
#      (.env.* 已被 .gitignore 忽略,真实 Key 不会进入仓库)
#   2. 部署时指定:
#        ENV_FILE=./backend/.env.prod ./deploy.sh
# ============================================================

于是 deploy.shdocker-compose.yml 之间就靠 ENV_FILE 这个变量串联:

yaml 复制代码
    env_file:
      # 部署时可指定环境配置:ENV_FILE=./backend/.env.prod ./deploy.sh
      # 本地开发默认使用 backend/.env
      - ${ENV_FILE:-./backend/.env}

${ENV_FILE:-./backend/.env} 是 compose 的默认值语法:不传就用开发配置,传了就用生产的。一行搞定两套环境。

第二条线:未配 Key 不阻断启动

.env 里的注释写得非常明确:

bash 复制代码
# ===== DeepSeek(Key 仅存后端,勿提交 git / 勿暴露前端)=====
# 请填入你自己的 Key;留空时 AI 相关功能不可用,其余模块不受影响
DEEPSEEK_API_KEY=

README 里也是同一条承诺:

复制 backend/.env 并填写需要的 Key(详见下节)。未配置 Key 的功能会自动降级,不会导致启动失败。

这个约定配合第 09 篇的四层降级才有意义 ------ 降级做扎实了,"缺 Key"就只是"某个功能不可用",而不是"整个服务起不来"。降级设计在这里从"容错"变成了"可配置性"的基础。

第三条线:容器内的地址归 compose 管

这是最容易出错的一条。.env 里写的是本地开发地址:

bash 复制代码
DB_URL=postgresql+asyncpg://admin:123456@127.0.0.1:5432/weather_db
REDIS_URL=redis://127.0.0.1:6379/0

但在容器里 127.0.0.1 指的是容器自己。所以 compose 用 environment覆盖

yaml 复制代码
    environment:
      # 容器内需用 compose 服务名互访,覆盖 .env 中本地开发的 127.0.0.1
      DB_URL: postgresql+asyncpg://admin:123456@postgres:5432/weather_db
      REDIS_URL: redis://redis:6379/0

.env.prod.example刻意不放这两个值,模板头部解释了原因:

bash 复制代码
# 与开发的区别:只放「会变的敏感配置」。数据库/Redis 连接串
# 由 docker-compose.yml 的 environment 段覆盖为容器网络地址,
# 不需要(也不应该)写在这里。

"不需要,也不应该" ------ 两套环境连的是不同主机,这个差异归编排层管;.env 只管"会变的敏感值"。

三段式的价值在于每一层只有一个职责 :模板告诉你缺什么,.env 放你的真实值,compose 管拓扑差异。混在一起就会出现"我明明改了 .env 为什么不生效"这种无从排查的问题。


三、Docker 起不来怎么办:原生进程兜底

这是很多个人项目的真实处境(我的机器就是缺 CPU 虚拟化支持,Docker Desktop 起不来)。项目有一组 PowerShell 脚本做兜底。

场景一:改一行文案,不想等镜像重建

dev-user.ps1 的注释直指痛点:

powershell 复制代码
#  为什么需要它:改一行文案就要重建镜像 + 重跑测试时,一轮十几分钟。
#  vite dev 是热更新------改完保存、浏览器刷新即可,秒级;而且开发模式
#  不启用 Service Worker,不会出现"改了没生效、其实是缓存"的经典坑。
#
#  两个地址的分工:
#    5173  开发用:热更新,改完即见(不跑测试、不用重建镜像)
#    8080  正式用:Docker 镜像里构建好的静态包,只在你验收/发布时才重建

"改了没生效,其实是缓存" ------ Service Worker 缓存是前端调试的经典陷阱。注释里点明"开发模式不启用 SW",把这条经验固化了。

脚本还顺手解决了 Node 版本问题:

powershell 复制代码
# 挑一个 Node 20+:优先 PATH,其次工具自带的多版本目录
...
if (-not $node) {
    Write-Host '未找到 Node 20+(Vite 8 跑不了 Node 18),请先安装 Node 20 或 22' -ForegroundColor Red
    exit 1
}

"Vite 8 跑不了 Node 18" ------ 与其让 Vite 抛一个难懂的错,不如启动前先检查版本、给一句人话。

场景二:Celery 在 Windows 上直接跑不起来

这是个高频坑。start_celery.ps1 的解法是多加一个 -P solo

powershell 复制代码
Start-Process -FilePath ".\venv\Scripts\python.exe" `
    -ArgumentList "-m", "celery", "-A", "app.celery_app", "worker", "-l", "info", "-P", "solo" `

-P solo 是 Celery 的单进程执行池。Windows 上默认的 prefork 池依赖 fork(),而 Windows 没有 fork------不加这个参数会直接起不来。

README 里也留了同一句提醒:

bash 复制代码
# Celery worker(Windows 需加 -P solo)
celery -A app.celery_app worker -l info -P solo

场景三:想给别人看,但没服务器

start-public.ps1 用 Cloudflare 免费隧道把本机端口暴露到公网:

powershell 复制代码
#  原理:项目用 docker compose 跑在本机,再通过 Cloudflare 免费隧道
#        (cloudflared quick tunnel)把本机端口暴露到公网。
#        无需服务器、无需备案、无需公网IP。

评论区里两个细节体现了安全意识:

powershell 复制代码
#   3. 管理端默认不对外暴露(含 admin 账号,暴露到公网有风险)
#      如需暴露,执行 .\start-public.ps1 -ExposeAdmin

默认安全,需要时显式打开 ------ 这是个好默认值。

还有一个探活判据的选择:

powershell 复制代码
# 用真实后端接口探活:前端 nginx 有 SPA 回退,访问 /health 也会返回 200,不能作判据
$r = Invoke-WebRequest -Uri "http://localhost:$UserPort/api/v1/weather/current" ...

"前端 nginx 有 SPA 回退,访问 /health 也会返回 200" ------ 这是很隐蔽的一个坑:用前端地址探活会永远"成功",因为任何路径都被 SPA 回退成 index.html 了。必须探一个真实的后端接口。


四、日志:一个 request_id 串起一次请求

第 03 篇讲过 contextvars 做用户隔离。同一个机制在这里用于可观测性------而且这次它解决的问题更直观。

observability.py 开头把三件事说得清清楚楚:

python 复制代码
"""可观测性基础设施:请求 ID 贯穿、结构化日志、轻量指标。

三个部件,解决三个问题:

1. **request_id 贯穿**
   每个请求生成唯一 ID(或继承上游的 X-Request-ID),通过 contextvars
   在整个调用链传播------包括所有 service 层日志。排查问题时「用一个 ID
   就能串起一次请求的全部日志」,不用再靠时间戳猜。

2. **结构化日志**
   日志输出为 JSON 单行(字段固定:ts/level/logger/msg/request_id +
   调用方通过 extra 传入的业务字段),便于日志采集与检索。

3. **轻量指标(/metrics)**
   进程内计数器:按路由统计请求数 / 错误数 / 总耗时。
   不引入 Prometheus 等依赖------当前「看调用量与错误率」的需求,
   几十行代码即可满足,等真有可视化需求时再升级采集方式,对外接口不变。
"""

"不用再靠时间戳猜" ------ 这句话就是全部动机。没有 request_id 时,排查靠"大概那个时间点附近的日志",有并发请求就混乱了。

怎么做到"所有日志都带 request_id"

关键是 logging.Filter------它能在每条日志落盘前注入字段:

python 复制代码
class RequestIdFilter(logging.Filter):
    """把 request_id 注入每条日志记录,供 Formatter 取用。"""

    def filter(self, record: logging.LogRecord) -> bool:
        record.request_id = request_id_var.get()
        return True

这就绕开了"每个 logger.info() 都要手动带 request_id"的麻烦 ------ 业务代码完全无感,只要它跑在请求上下文里,日志就自带 ID。

JSON 单行日志:extra 字段走白名单

python 复制代码
class JsonFormatter(logging.Formatter):
    """JSON 单行日志。

    extra 字段走白名单:业务代码通过 `logger.info("...", extra={"path": ...})`
    传业务字段,这里只输出白名单内的键------否则 logging 内部自带的
    几十个属性(funcName/lineno/msecs...)会把每行日志撑爆。
    """

    _EXTRA_KEYS = ("method", "path", "route", "status", "duration_ms",
                   "task_id", "task", "retries")

"否则会把每行日志撑爆" ------ 如果直接把 record.__dict__ 全序列化,每行会带上几十个 logging 内部属性(funcNamelinenomsecs...),日志体积翻好几倍。

有意思的是这个白名单里同时有 task_id / retries------HTTP 请求的字段和 Celery 任务的字段共用一份 formatter ,所以第 09 篇那个 task_failure 告警的结构化日志是直接能用的。

指标按"路由模板"聚合,不是真实 path

这是整个模块里我觉得最见功力的一处:

python 复制代码
# 用路由模板而不是真实 path 聚合指标------
# 否则 /news/84、/news/85 会被聚成两条,指标失去意义
route = getattr(request.scope.get("route"), "path", request.url.path)

如果没有这一行/api/v1/news/84/api/v1/news/85......每一个 ID 都会变成一条独立的指标记录。指标面板上会有几千条"只被访问过一次"的路由,完全失去统计意义

request.scope["route"].path 拿到的是路由模板/api/v1/news/{news_id}),所以所有同类请求会聚到一条上。

还有个容易忽略的细节------为什么不用 uvicorn 自带的 access log

python 复制代码
def setup_logging() -> None:
    """...
    关闭 uvicorn.access:请求日志由我们的中间件输出(带 request_id/耗时/路由),
    两份 access log 内容重复只会干扰检索。
    """
    logging.getLogger("uvicorn.access").disabled = True

配合 compose 里的启动参数:

yaml 复制代码
    # --no-access-log:请求日志由应用中间件输出(含 request_id/耗时/路由模板),
    # uvicorn 自带的 access log 与其重复且无 request_id,只会在日志里制造噪音
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --no-access-log

两处都要动 :代码里 disabled = True + 启动参数 --no-access-log。只做一处的话,另一路日志还是会冒出来。

最后还有一个 Windows 专属的小坑:

python 复制代码
    # Windows 下 stdout/stderr 重定向到文件时默认用本地编码(GBK),
    # JSON 里的中文会变成一堆问号;Linux 容器本来就是 UTF-8,reconfigure 无副作用
    for stream in (sys.stdout, sys.stderr):
        if hasattr(stream, "reconfigure"):
            with contextlib.suppress(Exception):
                stream.reconfigure(encoding="utf-8")

"JSON 里的中文会变成一堆问号" ------ 而 contextlib.suppress 是必要的防御:不是所有流都支持 reconfigure


五、CI:坏代码进不了主干

ci.yml 的三个层次,注释里表述得很精炼:

yaml 复制代码
# 分三个层次,由快到慢、由便宜到贵:
#   1. lint      几秒钟   ------ 格式、未使用变量、明显的坏味道
#   2. test      几分钟   ------ 442 个用例(后端 379 + 前端单测 41 + E2E 由本地/部署后跑)
#   3. build     几分钟   ------ 四个镜像能不能构建出来
#
# 为什么 lint 和 test 拆成不同 job:
#   lint 只要 10 秒,test 要 2 分钟。拆开后 lint 失败会立刻反馈,
#   不用等测试跑完;而且两者并行执行,总耗时取较慢的那个。

"lint 失败会立刻反馈" ------ 把快的放前面单独成 job,不是为了省时间(并行跑总时长取最慢的那个),而是为了让人更快知道最蠢的那类错误

发布和回滚的闭环

yaml 复制代码
  release:
    needs: [backend-lint, backend-test, frontend]
    if: github.event_name == 'push'
    permissions:
      contents: read
      packages: write

needs 保证"测试没过的代码不配被发布"。然后标签策略是回滚能力的基础:

yaml 复制代码
      # 标签策略(可回滚的关键):
      #   应用镜像 → latest(始终指向主干最新)+ sha-<短hash>(与提交一一对应)
      #   回滚时只要用旧提交的 sha-xxx tag 重新执行 deploy.sh 即可。
      #   postgres 镜像随 PG 版本走(pg17),不随应用版本变化,用固定 tag。

sha-<短hash> 是回滚的锚点。 只有 latest 的话,你无法回到"上一个具体版本"。

几个实用的 CI 细节

① 失败的日志要打进注解区,因为完整日志需要登录。

yaml 复制代码
      # 失败时把首尾日志输出成注解:pytest 的"用法错误"(exit 4) 会把原因
      # 写在最前面,而"测试失败"的信息在最后面,两头都取才够定位。
          head -12 pytest-output.log | while IFS= read -r line; do
            echo "::error::[头部] $line"
          done

"两头都取" ------ 这个观察很准:pytest 退出码 4(用法错误)的信息在最前面,而用例失败的信息在最后面。只 tail 会漏掉前者。

② 连发多次推送时,取消上一次没跑完的流水线。

yaml 复制代码
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true
# 好处:省额度,且不会出现"旧提交的检查结果覆盖新提交"的困惑

③ 前端的 pre-commit 钩子不能用 npm 命令。

yaml 复制代码
  # 用本地 Python 脚本而不是直接写 npm 命令,原因见脚本注释:
  # Windows 下 npm 实际是 npm.cmd,pre-commit 按字符串找可执行文件会找不到。

"npm 实际是 npm.cmd" ------ pre-commit 按可执行文件名查找,找不到 .cmd。这类平台差异只能靠踩坑积累。


六、小结与下一篇

可复用清单

场景 做法
环境配置 三段式:.env.example 模板 + .env 真实值(gitignore)+ compose 覆盖拓扑差异
敏感值 .gitignore 里忽略,但必须给模板文件,否则新人不知道配什么
缺配置 未配 Key 应降级而非阻断启动(前提是降级做扎实了)
上线脚本 备份 → 停应用 → 拉镜像 → 启动 → 健康检查 → 失败自动回滚
备份时机 放在停应用之后,数据不再变化时 dump 才是一致性快照
备份校验 非空 + 压缩完整性,别只检查文件存在------防"备份了个寂寞"
回滚 不重新拉镜像(旧镜像在本地),秒级完成;换 tag 就是回滚
演练 提供一个"强制失败"开关,但回滚后的检查必须真实探测
难懂的错误 启动前检查前置条件(Node 版本等),给一句人话而不是让框架抛异常
Windows + Celery worker 必须加 -P solo(默认 prefork 依赖 fork()
前端探活 别探前端地址------SPA 回退会让任何路径都返回 200,要探真实后端接口
请求日志 contextvars + logging.Filter 自动注入 request_id,业务代码无感
JSON 日志 extra 字段走白名单,不要全序列化(logging 内部属性会撑爆日志)
指标聚合 路由模板/news/{id})而不是真实 path,否则指标全是长尾
access log 代码里 disable + 启动参数 --no-access-log两处都要改
镜像标签 latest + sha-<短hash> 双标签,后者是回滚的锚点
CI 分层 快的单独成 job 先反馈;失败日志打注解(完整日志要登录才能看)

下一篇预告

运维这条线还能再往下走一层:数据库迁移与数据演化。项目用了 Alembic,但表结构里还有 TimescaleDB 超表、PostGIS 地理字段、pgvector 向量列这些"非标准 DDL"------它们怎么跟迁移脚本配合?改字段时历史数据怎么办?以及"测试库为什么每次都要重建"这个决定的真实代价。要的话我接着写,还是这个体量。


参考资料

  1. Docker Compose 官方文档 · Environment variables ------ ${VAR:-default} 默认值语法、env_fileenvironment 的优先级关系 docs.docker.com/compose/env...
  2. Cloudflare 官方文档 · TryCloudflare ------ 免账号的临时隧道(quick tunnel),域名每次变化 developers.cloudflare.com/cloudflare-...
  3. Celery 官方文档 · Workers 的 --pool 参数 ------ solo 池的适用场景(Windows 无 fork()docs.celeryq.dev/en/stable/u...
  4. Python 官方文档 · contextvars ------ ContextVar.set/reset 与 asyncio 的上下文隔离 docs.python.org/3/library/c...
  5. Python 官方文档 · logging.Filter ------ 在日志落盘前修改 LogRecord docs.python.org/3/library/l...
  6. GitHub Actions 官方文档 · Workflow 语法 ------ concurrency 取消并发运行、needs 依赖、::notice::::error:: 注解 docs.github.com/en/actions/...
  7. 项目源码(仍在更新中) ------ Ticnix/weather-travel-recommend-system github.com/Ticnix/weat...
相关推荐
正经教主1 小时前
【FDE系列】阶段2:Day 32:多表查询 — JOIN 与聚合
人工智能·python·fde
ArkPppp1 小时前
如何从零上线一个耐造的Redis缓存系统——最直接最不绕弯子的方式
后端·ai编程
Kyrie_kk1 小时前
Java--ProcessBuilder操作系统进程
java·后端
Ticnix1 小时前
迁移脚本能跑通,不代表你回滚得回来
后端·python
SamDeepThinking1 小时前
关于java final关键字的可见性
java·后端·面试
我的xiaodoujiao1 小时前
Django 基础知识详细图文教程 10-Django 模板引擎 3
后端·python·测试工具·django
拖孩1 小时前
给小程序加「一键发公众号贴图」,我把 wx.shareToOfficialAccount 的三个坑趟了一遍
前端·后端·微信小程序
quantdash_cc1 小时前
量化策略为什么需要实时行情数据?从信号产生到交易决策的时间差说起
开发语言·python·数据分析·量化交易·股票数据·quantdash
H Journey1 小时前
pip 和uv 开发和部署python项目
python·pip·uv