Compare commits

...

2 Commits

Author SHA1 Message Date
shanshanzhong147 f7f890c990 配置(#47,#51): CI/CD 安全加固 + TG 通知限制 + 部署健康检查
Build docker and publish / build (20.15.1) (push) Failing after 8m35s
- TG Bot Token/Chat ID 改用 secrets,移除硬编码
- PR 事件只构建不推镜像、不部署、不发通知
- 部署后增加健康检查,失败自动回滚
- 增加环境标签区分生产/测试/其他

Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 08:07:08 -07:00
shanshanzhong147 8b4e7561f4 修复: scripts 编译冲突 + order 统计 is_new 字段修正
- scripts/ 下两个独立脚本移到各自子目录,消除 package main 冲突
- model/order/model.go 统计查询 type→is_new 字段修正(7处)

Co-authored-by: multica-agent <github@multica.ai>
2026-05-26 08:06:50 -07:00
4 changed files with 158 additions and 60 deletions
+128 -30
View File
@@ -21,8 +21,8 @@ env:
# SSH私钥(Gitea Secret 名称:AWS # SSH私钥(Gitea Secret 名称:AWS
SSH_KEY: ${{ secrets.AWS }} SSH_KEY: ${{ secrets.AWS }}
# TG通知 # TG通知
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0 TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
TG_CHAT_ID: "-4940243803" TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}
# Go构建变量 # Go构建变量
SERVICE: vpn SERVICE: vpn
SERVICE_STYLE: vpn SERVICE_STYLE: vpn
@@ -51,16 +51,19 @@ jobs:
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
echo "DEPLOY_PATH=/opt/ppanel" >> $GITHUB_ENV echo "DEPLOY_PATH=/opt/ppanel" >> $GITHUB_ENV
echo "DEPLOY_ENV_LABEL=🚀 服务已成功部署到生产环境" >> $GITHUB_ENV
echo "为 main 分支设置生产环境变量" echo "为 main 分支设置生产环境变量"
elif [ "${{ github.ref_name }}" = "internal" ]; then elif [ "${{ github.ref_name }}" = "internal" ]; then
echo "DOCKER_TAG_SUFFIX=internal" >> $GITHUB_ENV echo "DOCKER_TAG_SUFFIX=internal" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server-internal" >> $GITHUB_ENV echo "CONTAINER_NAME=ppanel-server-internal" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV
echo "DEPLOY_ENV_LABEL=🧪 服务已成功部署到测试环境" >> $GITHUB_ENV
echo "为 internal 分支设置开发环境变量" echo "为 internal 分支设置开发环境变量"
else else
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server-${{ github.ref_name }}" >> $GITHUB_ENV echo "CONTAINER_NAME=ppanel-server-${{ github.ref_name }}" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/vpn_server_other" >> $GITHUB_ENV echo "DEPLOY_PATH=/root/vpn_server_other" >> $GITHUB_ENV
echo "DEPLOY_ENV_LABEL=🔧 服务已成功部署到其他环境" >> $GITHUB_ENV
echo "为其他分支 (${{ github.ref_name }}) 设置环境变量" echo "为其他分支 (${{ github.ref_name }}) 设置环境变量"
fi fi
@@ -112,24 +115,37 @@ jobs:
docker version || true docker version || true
echo "客户端 API 版本:" $(docker version --format '{{.Client.APIVersion}}') echo "客户端 API 版本:" $(docker version --format '{{.Client.APIVersion}}')
# 步骤4: 构建并发布到镜像仓库 # 步骤4: 构建镜像
- name: 📤 构建并发布到镜像仓库 - name: 🏗️ 构建镜像
run: | run: |
echo "开始构建并推送镜像..." echo "开始构建镜像..."
echo "仓库: ${{ env.REPO }}" echo "仓库: ${{ env.REPO }}"
echo "版本标签: ${{ env.VERSION }}" echo "版本标签: ${{ env.VERSION }}"
echo "分支标签: ${{ env.DOCKER_TAG_SUFFIX }}" echo "分支标签: ${{ env.DOCKER_TAG_SUFFIX }}"
# 构建镜像,同时打上版本和分支两个标签 BUILD_TAG_ARGS="-t ${{ env.REPO }}:${{ env.VERSION }}"
if [ "${{ github.event_name }}" = "push" ]; then
BUILD_TAG_ARGS="$BUILD_TAG_ARGS -t ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
else
echo "PR事件仅构建版本标签,不推送镜像、不部署"
fi
docker build -f Dockerfile \ docker build -f Dockerfile \
--platform linux/amd64 \ --platform linux/amd64 \
--build-arg TARGETARCH=amd64 \ --build-arg TARGETARCH=amd64 \
--build-arg VERSION=${{ env.VERSION }} \ --build-arg VERSION=${{ env.VERSION }} \
--build-arg BUILDTIME=${{ env.BUILDTIME }} \ --build-arg BUILDTIME=${{ env.BUILDTIME }} \
-t ${{ env.REPO }}:${{ env.VERSION }} \ $BUILD_TAG_ARGS \
-t ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }} \
. .
echo "镜像构建完成"
# 步骤5: 发布到镜像仓库
- name: 📤 发布到镜像仓库
if: github.event_name == 'push'
run: |
echo "开始推送镜像..."
echo "推送版本标签镜像: ${{ env.REPO }}:${{ env.VERSION }}" echo "推送版本标签镜像: ${{ env.REPO }}:${{ env.VERSION }}"
docker push ${{ env.REPO }}:${{ env.VERSION }} docker push ${{ env.REPO }}:${{ env.VERSION }}
@@ -138,8 +154,9 @@ jobs:
echo "镜像推送完成" echo "镜像推送完成"
# 调试: 打印部署目标(不输出敏感信息) # 步骤6: 调试 - 打印部署目标(不输出敏感信息)
- name: 🔍 调试 - 打印部署目标 - name: 🔍 调试 - 打印部署目标
if: github.event_name == 'push'
run: | run: |
echo "========== 部署目标调试 ==========" echo "========== 部署目标调试 =========="
echo "当前分支: ${{ github.ref_name }}" echo "当前分支: ${{ github.ref_name }}"
@@ -150,8 +167,9 @@ jobs:
echo "DEPLOY_PATH: ${{ env.DEPLOY_PATH }}" echo "DEPLOY_PATH: ${{ env.DEPLOY_PATH }}"
echo "=====================================" echo "====================================="
# 步骤5: 传输配置文件 # 步骤7: 传输配置文件
- name: 📂 传输配置文件 - name: 📂 传输配置文件
if: github.event_name == 'push'
uses: appleboy/scp-action@v0.1.7 uses: appleboy/scp-action@v0.1.7
with: with:
host: ${{ env.SSH_HOST }} host: ${{ env.SSH_HOST }}
@@ -161,8 +179,9 @@ jobs:
source: "docker-compose.cloud.yml" source: "docker-compose.cloud.yml"
target: "/tmp/ppanel-deploy/" target: "/tmp/ppanel-deploy/"
# 步骤6: 连接服务器更新并启动 # 步骤8: 连接服务器更新、健康检查并按需回滚
- name: 🚀 连接服务器更新并启动 - name: 🚀 连接服务器更新并启动
if: github.event_name == 'push'
uses: appleboy/ssh-action@v1.0.3 uses: appleboy/ssh-action@v1.0.3
with: with:
host: ${{ env.SSH_HOST }} host: ${{ env.SSH_HOST }}
@@ -172,36 +191,114 @@ jobs:
timeout: 300s timeout: 300s
command_timeout: 600s command_timeout: 600s
script: | script: |
set -e
echo "连接服务器成功,开始部署..." echo "连接服务器成功,开始部署..."
echo "部署目录: ${{ env.DEPLOY_PATH }}" echo "部署目录: ${{ env.DEPLOY_PATH }}"
echo "部署标签: ${{ env.DOCKER_TAG_SUFFIX }}" echo "部署标签: ${{ env.DOCKER_TAG_SUFFIX }}"
echo "登录用户: ${{ env.SSH_USER }}" echo "登录用户: ${{ env.SSH_USER }}"
HEALTHCHECK_URL="http://127.0.0.1:8080/v1/common/heartbeat"
NEW_TAG="${{ env.DOCKER_TAG_SUFFIX }}"
ROLLBACK_TAG="rollback-${{ env.VERSION }}"
SUDO=""
if [ "${{ github.ref_name }}" = "main" ]; then
SUDO="sudo"
fi
docker_cmd() {
if [ -n "$SUDO" ]; then
sudo docker "$@"
else
docker "$@"
fi
}
compose_with_tag() {
tag="$1"
shift
if [ -n "$SUDO" ]; then
sudo env PPANEL_SERVER_TAG="$tag" docker-compose -f docker-compose.cloud.yml "$@"
else
PPANEL_SERVER_TAG="$tag" docker-compose -f docker-compose.cloud.yml "$@"
fi
}
write_previous_tag() {
if [ -n "$SUDO" ]; then
printf '%s\n' "${PREVIOUS_IMAGE_TAG:-}" | sudo tee .previous-ppanel-image-tag >/dev/null
else
printf '%s\n' "${PREVIOUS_IMAGE_TAG:-}" > .previous-ppanel-image-tag
fi
}
health_check() {
attempt=1
while [ "$attempt" -le 3 ]; do
if curl -sf "$HEALTHCHECK_URL"; then
echo
return 0
fi
echo "健康检查第 ${attempt}/3 次失败,10s 后重试..."
attempt=$((attempt + 1))
sleep 10
done
return 1
}
if [ "${{ github.ref_name }}" = "main" ]; then if [ "${{ github.ref_name }}" = "main" ]; then
sudo mkdir -p ${{ env.DEPLOY_PATH }} sudo mkdir -p ${{ env.DEPLOY_PATH }}
sudo cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml sudo cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml
cd ${{ env.DEPLOY_PATH }}
echo "📥 拉取镜像..."
sudo docker-compose -f docker-compose.cloud.yml pull ppanel-server
echo "🚀 启动服务..."
sudo docker-compose -f docker-compose.cloud.yml up -d ppanel-server
sudo docker image prune -f || true
else else
mkdir -p ${{ env.DEPLOY_PATH }} mkdir -p ${{ env.DEPLOY_PATH }}
cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml
cd ${{ env.DEPLOY_PATH }}
echo "📥 拉取镜像..."
docker-compose -f docker-compose.cloud.yml pull ppanel-server
echo "🚀 启动服务..."
docker-compose -f docker-compose.cloud.yml up -d ppanel-server
docker image prune -f || true
fi fi
echo "✅ 部署命令执行完成" cd ${{ env.DEPLOY_PATH }}
# 步骤6: TG通知 (成功) PREVIOUS_IMAGE_TAG="$(docker_cmd inspect --format '{{.Config.Image}}' ppanel-server 2>/dev/null || true)"
PREVIOUS_IMAGE_ID="$(docker_cmd inspect --format '{{.Image}}' ppanel-server 2>/dev/null || true)"
echo "上一版本镜像tag: ${PREVIOUS_IMAGE_TAG:-未发现}"
echo "上一版本镜像ID: ${PREVIOUS_IMAGE_ID:-未发现}"
write_previous_tag
echo "📥 拉取镜像: ${{ env.REPO }}:${NEW_TAG}"
compose_with_tag "$NEW_TAG" pull ppanel-server
echo "🚀 启动服务..."
compose_with_tag "$NEW_TAG" up -d ppanel-server
echo "🩺 部署后健康检查: ${HEALTHCHECK_URL}"
if health_check; then
docker_cmd image prune -f || true
echo "✅ 部署后健康检查通过"
echo "✅ 部署命令执行完成"
exit 0
fi
echo "❌ 部署后健康检查连续 3 次失败,开始回滚..."
if [ -n "$PREVIOUS_IMAGE_ID" ]; then
docker_cmd tag "$PREVIOUS_IMAGE_ID" "${{ env.REPO }}:${ROLLBACK_TAG}"
echo "回滚镜像tag: ${{ env.REPO }}:${ROLLBACK_TAG}"
compose_with_tag "$ROLLBACK_TAG" up -d ppanel-server
echo "🩺 回滚后健康检查: ${HEALTHCHECK_URL}"
if health_check; then
echo "✅ 回滚后健康检查通过"
else
echo "❌ 回滚后健康检查仍失败"
fi
else
echo "未找到上一版本镜像ID,无法自动回滚"
fi
docker_cmd image prune -f || true
exit 1
# 步骤9: TG通知 (成功)
- name: 📱 发送成功通知到Telegram - name: 📱 发送成功通知到Telegram
if: success() if: success() && github.event_name == 'push'
uses: appleboy/telegram-action@master uses: appleboy/telegram-action@master
with: with:
token: ${{ env.TG_BOT_TOKEN }} token: ${{ env.TG_BOT_TOKEN }}
@@ -215,12 +312,13 @@ jobs:
👤 提交者: ${{ github.actor }} 👤 提交者: ${{ github.actor }}
🕐 时间: ${{ github.event.head_commit.timestamp }} 🕐 时间: ${{ github.event.head_commit.timestamp }}
🚀 服务已成功部署到生产环境 ${{ env.DEPLOY_ENV_LABEL }}
🩺 健康检查: 通过 (http://127.0.0.1:8080/v1/common/heartbeat)
parse_mode: Markdown parse_mode: Markdown
# 步骤5: TG通知 (失败) # 步骤10: TG通知 (失败)
- name: 📱 发送失败通知到Telegram - name: 📱 发送失败通知到Telegram
if: failure() if: failure() && github.event_name == 'push'
uses: appleboy/telegram-action@master uses: appleboy/telegram-action@master
with: with:
token: ${{ env.TG_BOT_TOKEN }} token: ${{ env.TG_BOT_TOKEN }}
@@ -234,6 +332,6 @@ jobs:
👤 提交者: ${{ github.actor }} 👤 提交者: ${{ github.actor }}
🕐 时间: ${{ github.event.head_commit.timestamp }} 🕐 时间: ${{ github.event.head_commit.timestamp }}
🩺 健康检查: 失败或未完成;若新版本健康检查连续 3 次失败,已自动尝试回滚并重新检查
⚠️ 请检查构建日志获取详细信息 ⚠️ 请检查构建日志获取详细信息
parse_mode: Markdown parse_mode: Markdown
+16 -16
View File
@@ -160,8 +160,8 @@ func (m *customOrderModel) QueryMonthlyOrders(ctx context.Context, date time.Tim
Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance"). Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance").
Select( Select(
"SUM(amount) as amount_total, " + "SUM(amount) as amount_total, " +
"SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) as new_order_amount, " + "SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " +
"SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) as renewal_order_amount", "SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount",
). ).
Scan(v).Error Scan(v).Error
}) })
@@ -177,8 +177,8 @@ func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time)
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?", []int64{2, 5}, dateStr, "balance"). Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?", []int64{2, 5}, dateStr, "balance").
Select( Select(
"SUM(amount) as amount_total, " + "SUM(amount) as amount_total, " +
"SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) as new_order_amount, " + "SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " +
"SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) as renewal_order_amount", "SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount",
). ).
Scan(v).Error Scan(v).Error
}) })
@@ -192,8 +192,8 @@ func (m *customOrderModel) QueryTotalOrders(ctx context.Context) (OrdersTotal, e
return conn.Model(&Order{}). return conn.Model(&Order{}).
Select(` Select(`
SUM(amount) AS amount_total, SUM(amount) AS amount_total,
SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS new_order_amount, SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) AS renewal_order_amount SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
`). `).
Where("status IN ? AND method != ?", []int64{2, 5}, "balance"). Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
Scan(&result).Error Scan(&result).Error
@@ -214,8 +214,8 @@ func (m *customOrderModel) QueryMonthlyUserCounts(ctx context.Context, date time
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
return conn.Model(&Order{}). return conn.Model(&Order{}).
Select(` Select(`
COUNT(DISTINCT CASE WHEN type = 1 THEN user_id END) AS new_users, COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN type = 2 THEN user_id END) AS renewal_users COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
`). `).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, firstDay, nextMonth, "balance"). []int64{2, 5}, firstDay, nextMonth, "balance").
@@ -232,8 +232,8 @@ func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Ti
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
return conn.Model(&Order{}). return conn.Model(&Order{}).
Select(` Select(`
COUNT(DISTINCT CASE WHEN type = 1 THEN user_id END) AS new_users, COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN type = 2 THEN user_id END) AS renewal_users COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
`). `).
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?", Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?",
[]int64{2, 5}, dateStr, "balance"). []int64{2, 5}, dateStr, "balance").
@@ -249,8 +249,8 @@ func (m *customOrderModel) QueryTotalUserCounts(ctx context.Context) (int64, int
return conn.Model(&Order{}). return conn.Model(&Order{}).
Where("status IN ? AND method != ?", []int64{2, 5}, "balance"). Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
Select(` Select(`
COUNT(DISTINCT CASE WHEN type = 1 THEN user_id END) AS new_users, COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN type = 2 THEN user_id END) AS renewal_users COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
`). `).
Scan(&counts).Error Scan(&counts).Error
}) })
@@ -282,8 +282,8 @@ func (m *customOrderModel) QueryDailyOrdersList(ctx context.Context, date time.T
Select(` Select(`
DATE_FORMAT(created_at, '%Y-%m-%d') AS date, DATE_FORMAT(created_at, '%Y-%m-%d') AS date,
SUM(amount) AS amount_total, SUM(amount) AS amount_total,
SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS new_order_amount, SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) AS renewal_order_amount SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
`). `).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, firstDay, nextDay, "balance"). []int64{2, 5}, firstDay, nextDay, "balance").
@@ -326,8 +326,8 @@ func (m *customOrderModel) QueryMonthlyOrdersList(ctx context.Context, date time
Select(` Select(`
DATE_FORMAT(created_at, '%Y-%m') AS date, DATE_FORMAT(created_at, '%Y-%m') AS date,
SUM(amount) AS amount_total, SUM(amount) AS amount_total,
SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS new_order_amount, SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) AS renewal_order_amount SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
`). `).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, start, end, "balance"). []int64{2, 5}, start, end, "balance").