diff --git a/CHANGELOG.md b/CHANGELOG.md index 41339b1..90e186e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [1.0.9] - 2026-01-19 + +### 移除 +- **移除 Webhook 自动部署功能** - 删除 webhook 服务及相关文件 + - 删除 `webhook/` 目录(Dockerfile、main.go、go.mod、README.md) + - 删除 `deploy.sh` 部署脚本 + - 删除 `WEBHOOK_SETUP.md` 配置文档 + - 移除 docker-compose.yaml 中的 webhook 服务配置 + ## [1.0.8] - 2026-01-18 ### 重构 diff --git a/README.md b/README.md index 00c9e0a..0c06346 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 一个基于微服务架构的个人账单分析工具,支持微信和支付宝账单的自动解析、智能分类和可视化分析。 -![版本](https://img.shields.io/badge/版本-1.0.8-green) +![版本](https://img.shields.io/badge/版本-1.0.9-green) ![架构](https://img.shields.io/badge/架构-微服务-blue) ![Go](https://img.shields.io/badge/Go-1.21-00ADD8) ![Python](https://img.shields.io/badge/Python-3.12-3776AB) @@ -20,7 +20,7 @@ - 🔍 **复核修正** - 对不确定的分类进行人工复核 - 💾 **数据持久化** - MongoDB 存储原始数据和清洗后数据,支持去重检查 - 🐳 **一键部署** - Docker Compose 快速启动全部服务 -- 🚀 **自动部署** - Gitea Webhook 触发零停机热更新 +- � **用户认证** - JWT Token 登录认证 ## 🏗️ 系统架构 @@ -129,13 +129,8 @@ BillAI/ │ │ └── wechat.py # 微信 │ └── Dockerfile │ -├── webhook/ # Webhook 服务 (Go) -│ ├── main.go # Webhook 入口 -│ └── Dockerfile -│ -├── deploy.sh # 自动部署脚本 -├── data/ # 测试数据目录 -├── mongo/ # MongoDB 数据 +├── mock_data/ # 测试数据目录 +├── mongodata/ # MongoDB 数据持久化 └── docker-compose.yaml # 容器编排 ``` @@ -275,10 +270,12 @@ python server.py | 版本 | 日期 | 主要更新 | |------|------|----------| -| **v1.0.7** | 2026-01-16 | 🔐 Token 过期由后端统一判断、401 自动退登;后端数据访问层收敛 | +| **v1.0.9** | 2026-01-19 | 🗑️ 移除 Webhook 自动部署功能 | +| **v1.0.8** | 2026-01-18 | ♻️ 前端账单模型统一为 UIBill、新增账单详情弹窗组件 | +| **v1.0.7** | 2026-01-16 | 🔐 Token 过期由后端统一判断、401 自动退登 | | **v1.0.6** | 2026-01-08 | 🐛 修复数据分析页面总支出和大盘数据错误 | -| **v1.0.5** | 2026-01-08 | 🐛 修复支付宝时间格式解析错误,修复WebHook编译错误 | -| **v1.0.4** | 2026-01-13 | 🚀 Gitea Webhook 自动部署、零停机热更新 | +| **v1.0.5** | 2026-01-08 | 🐛 修复支付宝时间格式解析错误 | +| **v1.0.4** | 2026-01-13 | ✨ MongoDB 数据持久化、上传去重 | | **v1.0.3** | 2026-01-13 | ✨ DateTimePicker 组件、收支分类动态切换 | | **v1.0.2** | 2026-01-11 | 🐛 修复时区和金额解析问题 | | **v1.0.1** | 2026-01-11 | 🐛 修复复核页面显示错误 | diff --git a/WEBHOOK_SETUP.md b/WEBHOOK_SETUP.md deleted file mode 100644 index e3b94aa..0000000 --- a/WEBHOOK_SETUP.md +++ /dev/null @@ -1,490 +0,0 @@ -# 自动部署配置指南 - -这个指南将帮助你配置 Gitea webhook 以实现代码推送时的自动部署。 - -## 整体架构 - -```mermaid -flowchart TD - A[🏠 Gitea
代码仓库] -->|Push Event| B[🔗 Webhook Service
Port 9000] - B -->|验证签名| C{签名有效?} - C -->|❌ 无效| D[拒绝请求] - C -->|✅ 有效| E{主分支?} - E -->|❌ 否| F[跳过部署] - E -->|✅ 是| G[📜 Deploy Script
deploy.sh] - G --> H[📥 git pull
拉取最新代码] - H --> I[🔨 热更新部署
零停机] - I --> J[🧹 清理旧镜像] - J --> K[🏥 健康检查] - K --> L[✅ 部署完成] - - style A fill:#f9f,stroke:#333 - style B fill:#bbf,stroke:#333 - style G fill:#bfb,stroke:#333 - style L fill:#9f9,stroke:#333 -``` - -## 第一步:生成 Webhook Secret - -打开终端,运行以下命令生成安全的随机密钥: - -```bash -# Linux / macOS -openssl rand -hex 32 - -# Windows PowerShell -[System.BitConverter]::ToString([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)).Replace('-','').ToLower() -``` - -示例输出: -``` -a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 -``` - -## 第二步:配置 docker-compose.yaml - -编辑 `docker-compose.yaml` 文件中的 webhook 服务配置: - -### 设置 Secret - -```yaml -webhook: - environment: - WEBHOOK_SECRET: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" # 替换为你生成的 Secret -``` - -### 配置仓库路径(关键) - -**重要!** 根据你的部署方式修改挂载路径: - -#### 如果使用本地路径部署: - -```yaml -webhook: - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /home/user/billai:/app # 替换为你的实际路径 -``` - -#### 如果在云服务器上部署: - -```yaml -webhook: - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - /opt/billai:/app # 替换为你的实际路径 -``` - -### 网络配置说明 - -**webhook 服务对外暴露 9000 端口**,用于接收 Gitea webhook 事件。 - -这样的好处是: -- ✅ 支持外部 Gitea 服务器的 webhook 回调 -- ✅ 灵活的部署架构 -- ✅ 支持多种网络拓扑 - -**Webhook 访问地址:** - -- 本地访问:`http://localhost:9000/webhook` -- 内部网络:`http://:9000/webhook` -- 公网访问:`http://:9000/webhook` -- 通过域名:`http://webhook.yourdomain.com:9000/webhook` - -## 第三步:启动 Webhook 服务 - -```bash -# 进入项目根目录 -cd /path/to/billai - -# 构建并启动 webhook 服务 -docker-compose up -d webhook - -# 验证服务是否启动 -docker-compose logs webhook - -# 测试健康检查 -curl http://localhost:9000/health -``` - -输出应该显示: -```json -{ - "status": "ok", - "time": "2026-01-13T10:30:00Z" -} -``` - -## 第四步:配置 Gitea Webhook - -### 4.1 进入 Gitea 仓库设置 - -1. 打开 Gitea 管理界面 -2. 找到你的 BillAI 仓库 -3. 点击 **设置 (Settings)** → **Webhooks** - -### 4.2 添加新 Webhook - -点击 "添加 Webhook (Add Webhook)" 按钮,选择 "Gitea" - -### 4.3 填写 Webhook 配置 - -| 字段 | 值 | -|------|-----| -| **目标 URL** | `http://:9000/webhook` | -| **内容类型** | `application/json` | -| **密钥** | `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6` (你生成的 Secret) | -| **事件** | ✓ 推送事件 (Push Events) | -| **活跃** | ✓ 是 | - -### 4.4 配置说明 - -#### 目标 URL - -根据 Gitea 部署方式选择合适的 URL: - -**情况 1:Gitea 和 webhook 都在同一台机器** - -``` -http://localhost:9000/webhook -``` - -**情况 2:Gitea 在不同机器,webhook 在 Docker** - -使用 webhook 所在服务器的 IP 地址: -``` -http://:9000/webhook -``` - -**情况 3:使用域名访问** - -``` -http://webhook.yourdomain.com:9000/webhook -或 -https://webhook.yourdomain.com/webhook (需要配置反向代理处理 HTTPS) -``` - -#### 防火墙配置 - -确保防火墙允许 9000 端口的入站连接: - -```bash -# Linux (ufw) -sudo ufw allow 9000/tcp - -# Linux (iptables) -sudo iptables -A INPUT -p tcp --dport 9000 -j ACCEPT - -# Windows Firewall (PowerShell) -New-NetFirewallRule -DisplayName "Allow Webhook" -Direction Inbound -LocalPort 9000 -Protocol TCP -Action Allow -``` - -### 4.5 测试 Webhook - -在 Gitea webhook 设置页面,找到刚添加的 webhook,点击 "测试 (Test)" 按钮。 - -预期响应: -```json -{ - "message": "非主分支,跳过部署" -} -``` - -(这是因为测试会使用一个默认的测试分支) - -## 第五步:网络配置 - -### 防火墙配置 - -在部署前,确保服务器防火墙允许以下端口: - -```bash -# 允许 9000 端口(webhook) -sudo ufw allow 9000/tcp - -# 允许 3000 端口(前端web)- 已配置 -sudo ufw allow 3000/tcp - -# 允许 80 端口(HTTP,如果使用) -sudo ufw allow 80/tcp - -# 允许 443 端口(HTTPS,如果使用) -sudo ufw allow 443/tcp -``` - -### 可选:Nginx 反向代理配置 - -如果需要通过 HTTPS 访问 webhook,可以配置 Nginx 反向代理: - -```nginx -server { - listen 443 ssl http2; - server_name webhook.yourdomain.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - location / { - proxy_pass http://localhost:9000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} - -# HTTP 重定向到 HTTPS -server { - listen 80; - server_name webhook.yourdomain.com; - return 301 https://$server_name$request_uri; -} -``` - -## 第六步:测试自动部署 - -现在你可以测试自动部署功能: - -```bash -# 进入仓库目录 -cd /path/to/billai - -# 做一些改动 -echo "# 测试自动部署" >> README.md - -# 提交并推送到主分支 -git add . -git commit -m "test: 测试自动部署功能" -git push origin master -``` - -### 监控部署过程 - -```bash -# 查看 webhook 服务日志 -docker-compose logs -f webhook - -# 查看部署脚本日志 -docker exec billai-webhook tail -f /tmp/billai_deploy.log - -# 查看其他服务日志 -docker-compose logs -f server -docker-compose logs -f web -docker-compose logs -f analyzer -``` - -## 常见问题排查 - -### 问题 1:Webhook 签名验证失败 - -**症状**:日志中出现 "无效的签名" - -**解决方案**: -- 确认 Gitea 中设置的 Secret 与 docker-compose.yaml 中的 WEBHOOK_SECRET 完全相同 -- 检查是否有多余空格或特殊字符 - -```bash -# 在 docker-compose.yaml 中查看 -grep WEBHOOK_SECRET docker-compose.yaml - -# 在 Gitea 中重新复制 Secret,确保没有多余空格 -``` - -### 问题 2:无法连接到 Webhook 服务 - -**症状**:Gitea webhook 测试失败 - -**解决方案**: -1. 检查 webhook 容器是否运行: -```bash -docker-compose ps webhook -``` - -2. 检查端口是否正确暴露: -```bash -docker-compose logs webhook | grep "listen" -``` - -3. 检查防火墙规则: -```bash -# Linux -netstat -tulpn | grep 9000 -sudo ufw status - -# Windows -netstat -ano | findstr 9000 -``` - -4. 测试本地连接: -```bash -curl http://localhost:9000/health -``` - -5. 如果是远程连接,测试网络连通性: -```bash -# 从 Gitea 服务器测试 -curl http://:9000/health - -netstat -ano | findstr 9000 # Windows -``` - -4. 测试本地连接: - -```bash -curl http://localhost:9000/health -``` - -### 问题 3:部署脚本无法执行 - -**症状**:日志中显示 "部署脚本执行失败" - -**解决方案**: -1. 检查 deploy.sh 是否存在: -```bash -ls -la deploy.sh -``` - -2. 检查文件权限: -```bash -chmod +x deploy.sh -``` - -3. 检查容器内的环境: -```bash -docker exec billai-webhook bash -c "ls -la /app/deploy.sh" -``` - -4. 查看详细错误信息: -```bash -docker exec billai-webhook tail -f /tmp/billai_deploy.log -``` - -### 问题 4:Docker 操作权限不足 - -**症状**:日志中显示 "permission denied" 或 "Cannot connect to Docker daemon" - -**解决方案**: -1. 检查 Docker Socket 是否正确挂载: -```yaml -webhook: - volumes: - - /var/run/docker.sock:/var/run/docker.sock -``` - -2. 检查 Docker Socket 权限: -```bash -ls -l /var/run/docker.sock -chmod 666 /var/run/docker.sock # 可能需要 sudo -``` - -3. 确保 webhook 容器以正确的用户运行(通常是 root) - -### 问题 5:部署后服务无法启动 - -**症状**:健康检查超时,部署失败 - -**解决方案**: -1. 检查 docker-compose build 是否成功: -```bash -docker-compose build -``` - -2. 检查依赖服务是否启动: -```bash -docker-compose ps -``` - -3. 查看各服务的详细日志: -```bash -docker-compose logs server -docker-compose logs web -docker-compose logs analyzer -``` - -4. 增加健康检查的超时时间(修改 deploy.sh 中的 sleep 时间) - -## 监控和维护 - -### 定期检查 Webhook 状态 - -```bash -# 查看最近 50 条日志 -docker-compose logs --tail=50 webhook - -# 实时监控日志 -docker-compose logs -f webhook -``` - -### 查看部署历史 - -部署脚本的日志保存在:`/tmp/billai_deploy.log` - -```bash -# 查看部署历史 -docker exec billai-webhook cat /tmp/billai_deploy.log - -# 持续监控 -docker exec billai-webhook tail -f /tmp/billai_deploy.log -``` - -### 禁用/启用自动部署 - -如果需要临时禁用自动部署,修改 deploy.sh: - -```bash -#!/bin/bash - -# 临时禁用部署,取消下面的注释 -# exit 0 - -# ... 后续脚本内容 -``` - -然后重新启动 webhook 服务: -```bash -docker-compose restart webhook -``` - -## 高级配置 - -### 只部署特定分支 - -编辑 webhook/main.go,修改分支检查逻辑: - -```go -// 检查分支,修改条件为只部署 master 和 develop 分支 -if !strings.Contains(payload.Ref, "master") && - !strings.Contains(payload.Ref, "develop") { - // 跳过部署 -} -``` - -### 添加 Slack 通知 - -修改 deploy.sh,在部署完成后发送 Slack 消息: - -```bash -# 在部署完成后添加 -curl -X POST -H 'Content-type: application/json' \ - --data "{\"text\":\"BillAI 部署完成!\"}" \ - YOUR_SLACK_WEBHOOK_URL -``` - -### 限制部署频率 - -在 webhook/main.go 中添加速率限制,防止频繁部署。 - -## 安全建议 - -1. **使用强密钥**:确保 WEBHOOK_SECRET 是随机生成的强密钥 -2. **限制网络访问**:只允许 Gitea 服务器的 IP 访问 webhook -3. **HTTPS**:在生产环境中使用 HTTPS 而不是 HTTP -4. **日志监控**:定期检查部署日志,及时发现问题 -5. **备份**:在部署前备份重要数据 - -## 相关文件 - -- [webhook/README.md](../webhook/README.md) - Webhook 服务文档 -- [deploy.sh](../deploy.sh) - 部署脚本 -- [docker-compose.yaml](../docker-compose.yaml) - Docker Compose 配置 -- [webhook/main.go](../webhook/main.go) - Webhook 服务源代码 diff --git a/deploy.sh b/deploy.sh deleted file mode 100644 index 7e00ea0..0000000 --- a/deploy.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/bin/bash - -# BillAI 自动部署脚本 -# 此脚本由 Gitea webhook 触发,自动执行 git pull 并重新构建部署 - -set -e - -REPO_ROOT="/app" -LOG_FILE="/tmp/billai_deploy.log" - -# 可由 webhook 传入:GIT_REF=refs/heads/main 或 refs/heads/master -GIT_REF="${GIT_REF:-}" -GIT_COMMIT="${GIT_COMMIT:-}" - -# 颜色输出 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -log() { - echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE" -} - -error() { - echo -e "${RED}[错误]${NC} $1" | tee -a "$LOG_FILE" - exit 1 -} - -success() { - echo -e "${GREEN}[成功]${NC} $1" | tee -a "$LOG_FILE" -} - -log "==========================================" -log "🚀 BillAI 自动部署开始" -log "==========================================" - -# 进入仓库目录 -cd "$REPO_ROOT" || error "无法进入仓库目录: $REPO_ROOT" -log "📁 工作目录: $(pwd)" - -if [ -n "$GIT_REF" ]; then - log "🌿 触发分支: $GIT_REF" -fi -if [ -n "$GIT_COMMIT" ]; then - log "🧾 触发提交: ${GIT_COMMIT:0:7}" -fi - -# 拉取最新代码 -log "📥 正在拉取最新代码..." -if ! git fetch origin; then - error "git fetch 失败" -fi - -# 选择部署分支(优先使用 webhook 传入的 ref) -DEPLOY_BRANCH="" -if [ "$GIT_REF" = "refs/heads/main" ]; then - DEPLOY_BRANCH="main" -elif [ "$GIT_REF" = "refs/heads/master" ]; then - DEPLOY_BRANCH="master" -fi - -# 兜底:按远端分支存在性选择(兼容仓库从 master 切到 main) -if [ -z "$DEPLOY_BRANCH" ]; then - if git show-ref --verify --quiet refs/remotes/origin/main; then - DEPLOY_BRANCH="main" - else - DEPLOY_BRANCH="master" - fi -fi - -log "🌿 部署分支: $DEPLOY_BRANCH" - -if ! git reset --hard "origin/$DEPLOY_BRANCH"; then - error "git reset 失败" -fi - -success "代码已更新" - -# 显示当前提交信息 -log "📝 当前提交:" -git log --oneline -1 - -# 检查 docker-compose 是否存在 -if [ ! -f "docker-compose.yaml" ]; then - error "docker-compose.yaml 不存在" -fi - -log "🐳 开始热更新部署(不停机)..." - -# 定义需要重新部署的服务(排除 webhook 自身,否则会自杀) -SERVICES="web server analyzer" - -# 拉取基础镜像更新 -log "📦 检查基础镜像更新..." -docker-compose pull mongodb mongo-express || true - -# 热更新:重新构建并替换容器(不停止旧服务,直接替换) -log "🔨 热更新服务(构建 + 替换)..." -if ! docker-compose up -d --build --force-recreate --no-deps $SERVICES; then - error "Docker 热更新失败" -fi - -# 清理旧的未使用镜像 -log "🧹 清理旧镜像..." -docker image prune -f || true - -# 等待服务启动 -log "⏳ 等待服务启动..." -sleep 5 - -# 检查服务健康状态 -log "🏥 检查服务健康状态..." -for i in {1..30}; do - if curl -f http://localhost:8080/health > /dev/null 2>&1; then - success "后端服务已启动" - break - fi - if [ $i -eq 30 ]; then - error "后端服务启动超时" - fi - sleep 1 -done - -for i in {1..30}; do - if curl -f http://localhost:3000 > /dev/null 2>&1; then - success "前端服务已启动" - break - fi - if [ $i -eq 30 ]; then - error "前端服务启动超时" - fi - sleep 1 -done - -log "==========================================" -success "🎉 部署完成" -log "==========================================" -log "⏰ 完成时间: $(date +'%Y-%m-%d %H:%M:%S')" -log "✅ 所有服务已启动并正常运行" -log "" -log "📍 访问地址:" -log " 前端: http://localhost:3000" -log " 后端: http://localhost:8080" -log "" diff --git a/docker-compose.yaml b/docker-compose.yaml index cedd96f..113c39e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -125,31 +125,3 @@ services: depends_on: mongodb: condition: service_healthy - - # Gitea Webhook 服务 - 自动部署 - webhook: - build: - context: ./webhook - dockerfile: Dockerfile - container_name: billai-webhook - restart: unless-stopped - ports: - - "9000:9000" - environment: - WEBHOOK_PORT: "9000" - # 重要:更改此 Secret 为强随机值 - # 生成方法: openssl rand -hex 32 - WEBHOOK_SECRET: "change-this-to-your-secret-key" - REPO_PATH: "/app" - volumes: - # 挂载 Docker Socket 以支持容器操作 - - /var/run/docker.sock:/var/run/docker.sock - # 挂载仓库目录 - - ./:/app - working_dir: /app - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/health"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 5s diff --git a/web/package.json b/web/package.json index 4ec79e7..8238068 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "web", "private": true, - "version": "1.0.8", + "version": "1.0.9", "type": "module", "scripts": { "dev": "vite dev", diff --git a/webhook/Dockerfile b/webhook/Dockerfile deleted file mode 100644 index 0a5a3e3..0000000 --- a/webhook/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -# Webhook 服务 Dockerfile -# 多阶段构建:编译阶段 + 运行阶段 - -# ===== 编译阶段 ===== -FROM golang:1.21-alpine AS builder - -WORKDIR /build - -# 配置 Alpine 镜像源(国内) -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories - -# 配置 Go 代理(国内镜像) -ENV GOPROXY=https://goproxy.cn,direct - -# 安装编译依赖 -RUN apk add --no-cache git - -# 复制代码 -COPY . . - -# 构建二进制 -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o webhook . - -# ===== 运行阶段 ===== -FROM alpine:3.19 - -WORKDIR /app - -# 配置 Alpine 镜像源(国内) -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories - -# 安装必要工具 -RUN apk add --no-cache bash curl git docker-cli docker-compose ca-certificates tzdata - -# 设置时区 -ENV TZ=Asia/Shanghai - -# 从编译阶段复制二进制 -COPY --from=builder /build/webhook /usr/local/bin/webhook - -# 注意:deploy.sh 通过 volume 挂载,不在构建时复制 -# 因为 deploy.sh 在仓库根目录,而 Dockerfile 在 webhook 目录 - -# 暴露端口 -EXPOSE 9000 - -# 健康检查 -HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ - CMD curl -f http://localhost:9000/health || exit 1 - -# 启动服务 -CMD ["webhook"] diff --git a/webhook/README.md b/webhook/README.md deleted file mode 100644 index efbc5f8..0000000 --- a/webhook/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# Webhook 服务 - -用于监听 Gitea webhook 事件,自动触发服务器拉取最新代码并重新构建部署。 - -## 特性 - -- ✅ 接收 Gitea webhook 推送事件 -- ✅ HMAC-SHA256 签名验证 -- ✅ 仅处理主分支 (master/main) 的推送 -- ✅ 自动执行部署脚本 -- ✅ 异步部署,快速响应 -- ✅ 零停机热更新 - -## 配置 - -### 环境变量 - -| 变量 | 说明 | 默认值 | -|-----|------|-------| -| `WEBHOOK_PORT` | webhook 服务监听端口 | 9000 | -| `WEBHOOK_SECRET` | Gitea webhook 签名密钥 | your-webhook-secret | -| `REPO_PATH` | 仓库根目录路径 | /app | - -### 部署脚本 - -脚本位置: `/app/deploy.sh` - -执行以下操作: -1. 拉取最新代码 (`git pull`) -2. 热更新部署(零停机) -3. 自动清理旧镜像 -4. 健康检查 - -## 安全性 - -### 设置 Webhook Secret - -在 Gitea 中配置 webhook 时,需要设置 Secret: - -1. 生成一个随机密钥: -```bash -openssl rand -hex 32 -# 输出示例: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 -``` - -2. 在 Docker 容器中设置环境变量: -```yaml -environment: - WEBHOOK_SECRET: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" -``` - -## Gitea Webhook 配置 - -### 1. 进入仓库设置 - -在 Gitea 中打开仓库,进入 **设置 → Webhook** - -### 2. 添加新 Webhook - -- **URL**: `http://:9000/webhook` -- **内容类型**: `application/json` -- **密钥**: 使用上面生成的 Secret -- **触发器**: 勾选 `推送事件` -- **活跃**: 是 - -### 3. 测试连接 - -点击 "测试" 按钮验证连接 - -## 日志 - -查看部署日志: - -```bash -# 查看容器日志 -docker logs billai-webhook - -# 查看部署脚本日志 -docker exec billai-webhook tail -f /tmp/billai_deploy.log -``` - -## 访问端点 - -- **Webhook**: `POST /webhook` -- **健康检查**: `GET /health` - -## 工作流 - -``` -Gitea Push Event - ↓ -Webhook Service (port 9000) - ↓ -验证 HMAC-SHA256 签名 - ↓ -检查分支 (仅主分支) - ↓ -异步触发部署脚本 - ↓ -执行 git pull - ↓ -执行 docker-compose build/up - ↓ -健康检查 - ↓ -部署完成 -``` - -## 故障排除 - -### 签名验证失败 - -- 检查 Gitea 中配置的 Secret 是否与环境变量 `WEBHOOK_SECRET` 一致 - -### 部署脚本无法执行 - -- 确保 `deploy.sh` 存在于仓库根目录 -- 检查文件权限:`chmod +x deploy.sh` -- 检查容器内是否有 git、docker 等工具 - -### Docker 操作失败 - -- 确保容器有权限访问 Docker Socket -- 在 docker-compose 中正确挂载 Docker Socket: -```yaml -volumes: - - /var/run/docker.sock:/var/run/docker.sock -``` - -## 示例 docker-compose 配置 - -参考项目根目录的 `docker-compose.yaml` diff --git a/webhook/go.mod b/webhook/go.mod deleted file mode 100644 index ae00ed1..0000000 --- a/webhook/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module webhook - -go 1.21 diff --git a/webhook/main.go b/webhook/main.go deleted file mode 100644 index cad582b..0000000 --- a/webhook/main.go +++ /dev/null @@ -1,282 +0,0 @@ -package main - -import ( - "bytes" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "io" - "log" - "net/http" - "os" - "os/exec" - "path/filepath" - "sync" - "time" -) - -// GiteaPayload Gitea webhook 推送事件 -type GiteaPayload struct { - Repository struct { - Name string `json:"name"` - FullName string `json:"full_name"` - CloneURL string `json:"clone_url"` - } `json:"repository"` - Pusher struct { - Username string `json:"username"` - } `json:"pusher"` - Ref string `json:"ref"` - Commit string `json:"after"` -} - -// Config webhook 服务配置 -type Config struct { - Port string - Secret string - RepoPath string - ScriptPath string - MaxBody int64 -} - -var cfg Config - -var deployMu sync.Mutex -var deployRunning bool - -const ( - defaultSecret = "your-webhook-secret" - maxBodyBytes = int64(1 << 20) // 1MB -) - -func init() { - // 从环境变量读取配置 - cfg.Port = os.Getenv("WEBHOOK_PORT") - if cfg.Port == "" { - cfg.Port = "9000" - } - - cfg.Secret = os.Getenv("WEBHOOK_SECRET") - if cfg.Secret == "" { - cfg.Secret = defaultSecret - } - - cfg.RepoPath = os.Getenv("REPO_PATH") - if cfg.RepoPath == "" { - cfg.RepoPath = "/app" - } - - cfg.ScriptPath = filepath.Join(cfg.RepoPath, "deploy.sh") - cfg.MaxBody = maxBodyBytes -} - -func main() { - log.SetFlags(log.LstdFlags | log.Lshortfile) - if cfg.Secret == "" || cfg.Secret == defaultSecret { - log.Fatalf("❌ WEBHOOK_SECRET 未配置或仍为默认值;请设置强随机 secret(例如 openssl rand -hex 32)") - } - - log.Printf("🚀 Webhook 服务启动 | 监听端口: %s\n", cfg.Port) - log.Printf("📁 仓库路径: %s\n", cfg.RepoPath) - log.Printf("📜 部署脚本: %s\n", cfg.ScriptPath) - log.Printf("🔒 请求体大小限制: %d bytes\n", cfg.MaxBody) - - mux := http.NewServeMux() - mux.HandleFunc("/webhook", handleWebhook) - mux.HandleFunc("/health", handleHealth) - - srv := &http.Server{ - Addr: ":" + cfg.Port, - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, - ReadTimeout: 10 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 60 * time.Second, - } - - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("❌ 服务器启动失败: %v", err) - } -} - -// handleHealth 健康检查端点 -func handleHealth(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{ - "status": "ok", - "time": time.Now().Format(time.RFC3339), - }) -} - -// handleWebhook 处理 Gitea webhook -func handleWebhook(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // 单实例部署:避免并发执行 deploy.sh 引发互相踩踏 - if !tryStartDeploy() { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Retry-After", "30") - w.WriteHeader(http.StatusConflict) - json.NewEncoder(w).Encode(map[string]string{ - "message": "部署进行中,请稍后再试", - }) - return - } - defer finishDeploy() - - // 限制请求体大小,防止大包占用内存 - r.Body = http.MaxBytesReader(w, r.Body, cfg.MaxBody) - - // 读取请求体 - body, err := io.ReadAll(r.Body) - if err != nil { - log.Printf("❌ 读取请求体失败: %v", err) - http.Error(w, "Failed to read body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - // 验证签名 - if !verifySignature(r.Header.Get("X-Gitea-Signature"), body) { - log.Println("⚠️ 无效的签名") - http.Error(w, "Invalid signature", http.StatusUnauthorized) - return - } - - // 解析 payload - var payload GiteaPayload - if err := json.Unmarshal(body, &payload); err != nil { - log.Printf("❌ 解析 JSON 失败: %v", err) - http.Error(w, "Failed to parse JSON", http.StatusBadRequest) - return - } - - // 只处理主分支推送(严格匹配) - if payload.Ref != "refs/heads/master" && payload.Ref != "refs/heads/main" { - log.Printf("⏭️ 忽略非主分支推送: %s", payload.Ref) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "message": "非主分支,跳过部署", - }) - return - } - - log.Printf("✅ 收到有效的 Webhook 请求") - log.Printf(" 仓库: %s", payload.Repository.FullName) - log.Printf(" 推送者: %s", payload.Pusher.Username) - log.Printf(" 分支: %s", payload.Ref) - commitShort := payload.Commit - if len(commitShort) > 7 { - commitShort = commitShort[:7] - } - log.Printf(" 提交: %s", commitShort) - - // 异步执行部署(释放 HTTP 线程) - go func() { - defer func() { - // 避免 goroutine panic 导致“卡住 running 状态” - if r := recover(); r != nil { - log.Printf("❌ 部署流程 panic: %v", r) - } - }() - executeDeployment(payload) - }() - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusAccepted) - json.NewEncoder(w).Encode(map[string]string{ - "message": "部署已启动", - "commit": payload.Commit, - }) -} - -// verifySignature 验证 Gitea webhook 签名 -func verifySignature(signature string, body []byte) bool { - if signature == "" { - return false - } - - hash := hmac.New(sha256.New, []byte(cfg.Secret)) - hash.Write(body) - expected := []byte("sha256=" + hex.EncodeToString(hash.Sum(nil))) - return hmac.Equal([]byte(signature), expected) -} - -// executeDeployment 执行部署 -func executeDeployment(payload GiteaPayload) { - log.Println("\n🔄 开始部署流程...") - - startTime := time.Now() - - // 检查脚本是否存在 - if _, err := os.Stat(cfg.ScriptPath); os.IsNotExist(err) { - log.Printf("❌ 部署脚本不存在: %s", cfg.ScriptPath) - return - } - - // 执行部署脚本(把 ref/commit 传给脚本,便于按分支部署与日志记录) - cmd := exec.Command("bash", cfg.ScriptPath) - cmd.Dir = cfg.RepoPath - cmd.Env = append(os.Environ(), - "GIT_REF="+payload.Ref, - "GIT_COMMIT="+payload.Commit, - ) - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - log.Printf("📜 执行部署脚本: %s", cfg.ScriptPath) - - if err := cmd.Run(); err != nil { - log.Printf("❌ 部署脚本执行失败: %v", err) - if stdout.Len() > 0 { - log.Printf("输出:\n%s", stdout.String()) - } - if stderr.Len() > 0 { - log.Printf("错误输出:\n%s", stderr.String()) - } - return - } - - duration := time.Since(startTime).Seconds() - - log.Printf("✅ 部署完成") - log.Printf(" 耗时: %.2f 秒", duration) - - if stdout.Len() > 0 { - log.Printf("输出:\n%s", stdout.String()) - } - - log.Printf("📝 部署日志:\n") - log.Printf(" 仓库: %s", payload.Repository.FullName) - commitShort := payload.Commit - if len(commitShort) > 7 { - commitShort = commitShort[:7] - } - log.Printf(" 提交: %s", commitShort) - log.Printf(" 分支: %s", payload.Ref) - log.Printf(" 推送者: %s", payload.Pusher.Username) - log.Printf(" 时间: %s", time.Now().Format(time.RFC3339)) -} - -func tryStartDeploy() bool { - deployMu.Lock() - defer deployMu.Unlock() - if deployRunning { - return false - } - deployRunning = true - return true -} - -func finishDeploy() { - deployMu.Lock() - deployRunning = false - deployMu.Unlock() -}