This commit is contained in:
wenfp
2026-07-27 12:53:13 +08:00
parent 5ecf77335e
commit 31a7b08f03
5 changed files with 79 additions and 39 deletions
+1
View File
@@ -7,5 +7,6 @@ DEPLOY_BRANCH=main
DEPLOY_SCRIPT_PATH=/www/wwwroot/next.zizaifutian.com/admin/scripts/deploy_front.sh DEPLOY_SCRIPT_PATH=/www/wwwroot/next.zizaifutian.com/admin/scripts/deploy_front.sh
DEPLOY_COMMAND_TIMEOUT=600 DEPLOY_COMMAND_TIMEOUT=600
DEPLOY_LOCK_PATH=/tmp/py-web-deploy.lock DEPLOY_LOCK_PATH=/tmp/py-web-deploy.lock
DEPLOY_LOG_PATH=/tmp/py-web-deploy.log
# 可选:填服务器 `dirname $(which node)` 的结果,解决 webhook 进程拿到旧 Node 的问题。 # 可选:填服务器 `dirname $(which node)` 的结果,解决 webhook 进程拿到旧 Node 的问题。
DEPLOY_NODE_BIN_DIR=/usr/bin DEPLOY_NODE_BIN_DIR=/usr/bin
+2 -1
View File
@@ -92,7 +92,7 @@ pnpm dev
- 新增后端业务模块时,优先新增 `app/routers/<module>.py` 和对应 `app/schemas.py` 模型。 - 新增后端业务模块时,优先新增 `app/routers/<module>.py` 和对应 `app/schemas.py` 模型。
- 新增前端业务模块时,优先新增 `front/src/views/<module>/``front/src/api/<module>.ts`,并在 `front/src/router/index.ts` 注册路由。 - 新增前端业务模块时,优先新增 `front/src/views/<module>/``front/src/api/<module>.ts`,并在 `front/src/router/index.ts` 注册路由。
- 登录认证已接入,后台路由由前端守卫检查登录状态;认证令牌由后端 HttpOnly Cookie 管理,前端不保存 token。 - 登录认证已接入,后台路由由前端守卫检查登录状态;认证令牌由后端 HttpOnly Cookie 管理,前端不保存 token。
- 自动部署接口为 `/deploy/webhook`,生产环境经 Nginx 的 `/api/deploy/webhook` 暴露给 Gitea;部署密钥、项目目录、前端目录、部署分支脚本路径通过 `.env` 或环境变量配置。 - 自动部署接口为 `/deploy/webhook`,生产环境经 Nginx 的 `/api/deploy/webhook` 暴露给 Gitea;部署密钥、项目目录、前端目录、部署分支脚本路径和日志路径通过 `.env` 或环境变量配置。
## 修改记录约定 ## 修改记录约定
@@ -116,3 +116,4 @@ pnpm dev
- 2026-07-23:商品模型新增商品描述字段,后端补充 `products.description` 迁移、Pydantic 模型和 CRUD 读写,前端商品 API 类型与商品编辑基础信息表单同步。验证:后端编译通过,前端 `pnpm typecheck``pnpm build` 通过,运行态接口测试待安装 `bcrypt` 后执行。 - 2026-07-23:商品模型新增商品描述字段,后端补充 `products.description` 迁移、Pydantic 模型和 CRUD 读写,前端商品 API 类型与商品编辑基础信息表单同步。验证:后端编译通过,前端 `pnpm typecheck``pnpm build` 通过,运行态接口测试待安装 `bcrypt` 后执行。
- 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,后端增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位,前端订单管理页增加微信预下单入口;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,前端 `pnpm typecheck``pnpm build` 通过,真实预下单和支付通知待填写微信商户配置后联调。 - 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,后端增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位,前端订单管理页增加微信预下单入口;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,前端 `pnpm typecheck``pnpm build` 通过,真实预下单和支付通知待填写微信商户配置后联调。
- 2026-07-27:新增 Gitea 自动部署 webhook,后端 `/deploy/webhook` 校验 Gitea HMAC 签名或部署 token 后执行 `scripts/deploy_front.sh`,脚本负责拉取代码、安装前端依赖并执行 `pnpm build`;新增 `.env.example` 记录部署环境变量。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。 - 2026-07-27:新增 Gitea 自动部署 webhook,后端 `/deploy/webhook` 校验 Gitea HMAC 签名或部署 token 后执行 `scripts/deploy_front.sh`,脚本负责拉取代码、安装前端依赖并执行 `pnpm build`;新增 `.env.example` 记录部署环境变量。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。
- 2026-07-27:自动部署 webhook 从同步构建调整为后台构建,接口校验通过后立即返回 `202 accepted`,后台执行结果写入 `DEPLOY_LOG_PATH`,避免 Gitea Delivery 等待前端构建超时。验证:后端编译通过,webhook 后台任务触发测试通过。
+3 -2
View File
@@ -88,8 +88,8 @@ OSS_ENDPOINT、OSS_BUCKET_NAME、OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS
- 自动部署接口为 `/deploy/webhook`,生产环境通常通过 Nginx 映射为 `/api/deploy/webhook` 给 Gitea 调用。 - 自动部署接口为 `/deploy/webhook`,生产环境通常通过 Nginx 映射为 `/api/deploy/webhook` 给 Gitea 调用。
- Gitea Webhook 应配置 `Secret`,后端会按原始请求体校验 `X-Gitea-Signature` 的 HMAC-SHA256 签名;也兼容 `X-Hub-Signature-256` - Gitea Webhook 应配置 `Secret`,后端会按原始请求体校验 `X-Gitea-Signature` 的 HMAC-SHA256 签名;也兼容 `X-Hub-Signature-256`
- 部署配置通过 `.env` 或环境变量提供:`DEPLOY_WEBHOOK_SECRET``DEPLOY_PROJECT_DIR``DEPLOY_FRONT_DIR``DEPLOY_BRANCH``DEPLOY_SCRIPT_PATH``DEPLOY_COMMAND_TIMEOUT``DEPLOY_LOCK_PATH`;如 webhook 进程拿到旧 Node,可设置 `DEPLOY_NODE_BIN_DIR` 指向新 Node 所在目录。 - 部署配置通过 `.env` 或环境变量提供:`DEPLOY_WEBHOOK_SECRET``DEPLOY_PROJECT_DIR``DEPLOY_FRONT_DIR``DEPLOY_BRANCH``DEPLOY_SCRIPT_PATH``DEPLOY_COMMAND_TIMEOUT``DEPLOY_LOCK_PATH``DEPLOY_LOG_PATH`;如 webhook 进程拿到旧 Node,可设置 `DEPLOY_NODE_BIN_DIR` 指向新 Node 所在目录。
- 部署脚本为 `scripts/deploy_front.sh`,负责在服务器项目目录执行 `git pull`、进入前端目录安装依赖并执行 `pnpm build` - 部署脚本为 `scripts/deploy_front.sh`,负责在服务器项目目录执行 `git pull`、进入前端目录安装依赖并执行 `pnpm build`;webhook 接口只投递后台任务并立即返回 `202 accepted`,构建结果查看 `DEPLOY_LOG_PATH`
- webhook 密钥不得写入源码或文档;`.env.example` 只能保留占位值。 - webhook 密钥不得写入源码或文档;`.env.example` 只能保留占位值。
## 验证 ## 验证
@@ -113,3 +113,4 @@ OSS_ENDPOINT、OSS_BUCKET_NAME、OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS
- 2026-07-23:商品模型新增 `description` 商品描述字段,后端补充 `products.description` 轻量迁移、Pydantic 模型和 `/products` CRUD 读写。验证:后端编译通过,运行态接口测试待安装 `bcrypt` 后执行。 - 2026-07-23:商品模型新增 `description` 商品描述字段,后端补充 `products.description` 轻量迁移、Pydantic 模型和 `/products` CRUD 读写。验证:后端编译通过,运行态接口测试待安装 `bcrypt` 后执行。
- 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,真实预下单和通知验签待填写微信商户配置后联调。 - 2026-07-23:新增微信支付 JSAPI 预下单和支付通知处理,增加 `payment_transactions` 支付流水表、微信支付 API v3 签名/通知验签/回调解密工具和配置占位;`requirements.txt` 新增 `cryptography`。验证:后端编译通过,真实预下单和通知验签待填写微信商户配置后联调。
- 2026-07-27:新增自动部署 webhook 模块,`/deploy/webhook` 支持 Gitea HMAC 签名校验、部署分支过滤、并发锁和构建脚本超时控制,`scripts/deploy_front.sh` 负责拉取代码并构建前端。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。 - 2026-07-27:新增自动部署 webhook 模块,`/deploy/webhook` 支持 Gitea HMAC 签名校验、部署分支过滤、并发锁和构建脚本超时控制,`scripts/deploy_front.sh` 负责拉取代码并构建前端。验证:后端编译通过,webhook 密钥与 Gitea 签名校验测试通过。
- 2026-07-27:自动部署 webhook 改为后台执行构建,接口在签名校验、分支检查和加锁后立即返回 `202 accepted`,后台任务将构建结果写入部署日志,避免 Gitea Delivery 因等待响应超时。验证:后端编译通过,webhook 后台任务触发测试通过。
+6
View File
@@ -55,6 +55,8 @@ DEPLOY_BRANCH = os.getenv("DEPLOY_BRANCH", "")
DEPLOY_SCRIPT_PATH = os.getenv("DEPLOY_SCRIPT_PATH", str(PROJECT_ROOT / "scripts" / "deploy_front.sh")) DEPLOY_SCRIPT_PATH = os.getenv("DEPLOY_SCRIPT_PATH", str(PROJECT_ROOT / "scripts" / "deploy_front.sh"))
DEPLOY_COMMAND_TIMEOUT = int(os.getenv("DEPLOY_COMMAND_TIMEOUT", "600")) DEPLOY_COMMAND_TIMEOUT = int(os.getenv("DEPLOY_COMMAND_TIMEOUT", "600"))
DEPLOY_LOCK_PATH = os.getenv("DEPLOY_LOCK_PATH", "/tmp/py-web-deploy.lock") DEPLOY_LOCK_PATH = os.getenv("DEPLOY_LOCK_PATH", "/tmp/py-web-deploy.lock")
DEPLOY_LOG_PATH = os.getenv("DEPLOY_LOG_PATH", "/tmp/py-web-deploy.log")
DEPLOY_NODE_BIN_DIR = os.getenv("DEPLOY_NODE_BIN_DIR", "")
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -160,6 +162,8 @@ class DeploySettings:
script_path: str script_path: str
command_timeout: int command_timeout: int
lock_path: str lock_path: str
log_path: str
node_bin_dir: str
@property @property
def is_configured(self) -> bool: def is_configured(self) -> bool:
@@ -174,4 +178,6 @@ deploy_settings = DeploySettings(
script_path=DEPLOY_SCRIPT_PATH, script_path=DEPLOY_SCRIPT_PATH,
command_timeout=DEPLOY_COMMAND_TIMEOUT, command_timeout=DEPLOY_COMMAND_TIMEOUT,
lock_path=DEPLOY_LOCK_PATH, lock_path=DEPLOY_LOCK_PATH,
log_path=DEPLOY_LOG_PATH,
node_bin_dir=DEPLOY_NODE_BIN_DIR,
) )
+67 -36
View File
@@ -2,11 +2,12 @@ import hashlib
import hmac import hmac
import os import os
import subprocess import subprocess
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import parse_qs from urllib.parse import parse_qs
from fastapi import APIRouter, HTTPException, Request, status from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
from app.config import deploy_settings from app.config import deploy_settings
@@ -113,8 +114,66 @@ def release_lock(fd: int) -> None:
pass pass
@router.post("/webhook") def append_deploy_log(content: str) -> None:
async def deploy_webhook(request: Request) -> dict[str, Any]: log_path = Path(deploy_settings.log_path)
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open("a", encoding="utf-8") as file:
file.write(content)
if not content.endswith("\n"):
file.write("\n")
def build_deploy_env() -> dict[str, str]:
env = os.environ.copy()
env.update(
{
"DEPLOY_PROJECT_DIR": deploy_settings.project_dir,
"DEPLOY_FRONT_DIR": deploy_settings.front_dir,
"DEPLOY_BRANCH": deploy_settings.branch,
"DEPLOY_NODE_BIN_DIR": deploy_settings.node_bin_dir,
}
)
return env
def run_deploy_task(lock_fd: int) -> None:
started_at = datetime.utcnow().replace(microsecond=0).isoformat()
append_deploy_log(f"\n[{started_at}] deploy started")
try:
result = subprocess.run(
[deploy_settings.script_path],
cwd=deploy_settings.project_dir,
env=build_deploy_env(),
capture_output=True,
text=True,
timeout=deploy_settings.command_timeout,
check=False,
)
finished_at = datetime.utcnow().replace(microsecond=0).isoformat()
append_deploy_log(
"\n".join(
[
f"[{finished_at}] deploy finished returncode={result.returncode}",
"--- stdout ---",
output_tail(result.stdout, 20000),
"--- stderr ---",
output_tail(result.stderr, 20000),
"--- end ---",
]
)
)
except subprocess.TimeoutExpired:
finished_at = datetime.utcnow().replace(microsecond=0).isoformat()
append_deploy_log(f"[{finished_at}] deploy timeout after {deploy_settings.command_timeout}s")
except Exception as exc:
finished_at = datetime.utcnow().replace(microsecond=0).isoformat()
append_deploy_log(f"[{finished_at}] deploy error: {exc}")
finally:
release_lock(lock_fd)
@router.post("/webhook", status_code=status.HTTP_202_ACCEPTED)
async def deploy_webhook(request: Request, background_tasks: BackgroundTasks) -> dict[str, Any]:
body = await request.body() body = await request.body()
verify_webhook_secret(request, body) verify_webhook_secret(request, body)
@@ -134,38 +193,10 @@ async def deploy_webhook(request: Request) -> dict[str, Any]:
) )
lock_fd = acquire_lock() lock_fd = acquire_lock()
try: background_tasks.add_task(run_deploy_task, lock_fd)
env = os.environ.copy()
env.update(
{
"DEPLOY_PROJECT_DIR": deploy_settings.project_dir,
"DEPLOY_FRONT_DIR": deploy_settings.front_dir,
"DEPLOY_BRANCH": deploy_settings.branch,
}
)
result = subprocess.run(
[str(script_path)],
cwd=deploy_settings.project_dir,
env=env,
capture_output=True,
text=True,
timeout=deploy_settings.command_timeout,
check=False,
)
except subprocess.TimeoutExpired as exc:
raise HTTPException(
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
detail=f"部署命令超过 {deploy_settings.command_timeout} 秒未完成",
) from exc
finally:
release_lock(lock_fd)
response = { return {
"status": "success" if result.returncode == 0 else "failed", "status": "accepted",
"returncode": result.returncode, "message": "已接收推送事件,前端构建已在后台开始",
"stdout": output_tail(result.stdout), "log_path": deploy_settings.log_path,
"stderr": output_tail(result.stderr),
} }
if result.returncode != 0:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=response)
return response