From 12fa2b0d19e06b101fe5d0b1c3eb0d280f815f2e Mon Sep 17 00:00:00 2001 From: gouki Date: Sun, 18 Oct 2026 11:31:51 +0000 Subject: [PATCH] =?UTF-8?q?refactor(server):=20=E6=8B=86=E5=88=86=20cmd/mi?= =?UTF-8?q?ni=20=E4=B8=8E=20cmd/web=20=E4=B8=A4=E4=B8=AA=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 小程序接口(lunar.neatcn.com)与商业版 Web(kepo.net)此前共用一个 二进制,导致小程序域名暴露商业后台与 Stripe Webhook,商业站点也 暴露了许愿树等小程序接口。现拆为两个入口: - cmd/mini:微信登录、生辰档案、许愿树、百科、版本号 - cmd/web:邮箱登录、commerce、报告/订单、/admin 后台、商业前端 静态托管与 SPA 回退 - internal/app 承载共用启动与路由注册;图片资源移至 internal/assets CI:server-deploy.yml 改构建 cmd/mini 并随包移除后台静态资源; 新增 web-deploy.yml 构建 cmd/web + web-commercial 前端,发布 web-v* Release;deploy-watch.sh 支持 TAG_PREFIX/ASSET_NAME 变量; 新增 deploy-web.sh(systemd 版)与 kepo 一次性配置文档 .gitea/docs/PULL_DEPLOY_WEB.md。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .gitea/docs/PULL_DEPLOY_WEB.md | 137 +++++++++ .gitea/scripts/deploy-watch.sh | 17 +- .gitea/scripts/deploy-web.sh | 130 +++++++++ .gitea/workflows/server-deploy.yml | 10 +- .gitea/workflows/web-deploy.yml | 270 ++++++++++++++++++ AGENTS.md | 3 + server/README.md | 17 +- server/cmd/main.go | 199 ------------- server/cmd/mini/main.go | 30 ++ server/cmd/web/main.go | 31 ++ server/internal/app/app.go | 51 ++++ server/internal/app/mini.go | 79 +++++ server/internal/app/web.go | 102 +++++++ server/internal/assets/assets.go | 8 + .../assets/images/trees/tree-1.jpg | Bin 15 files changed, 872 insertions(+), 212 deletions(-) create mode 100644 .gitea/docs/PULL_DEPLOY_WEB.md create mode 100644 .gitea/scripts/deploy-web.sh create mode 100644 .gitea/workflows/web-deploy.yml delete mode 100644 server/cmd/main.go create mode 100644 server/cmd/mini/main.go create mode 100644 server/cmd/web/main.go create mode 100644 server/internal/app/app.go create mode 100644 server/internal/app/mini.go create mode 100644 server/internal/app/web.go create mode 100644 server/internal/assets/assets.go rename server/{cmd => internal}/assets/images/trees/tree-1.jpg (100%) diff --git a/.gitea/docs/PULL_DEPLOY_WEB.md b/.gitea/docs/PULL_DEPLOY_WEB.md new file mode 100644 index 0000000..e4a7198 --- /dev/null +++ b/.gitea/docs/PULL_DEPLOY_WEB.md @@ -0,0 +1,137 @@ +# kepo.net 商业版 Web 拉取式部署配置(一次性) + +> 安全原则同 [PULL_DEPLOY.md](./PULL_DEPLOY.md):CI 只构建、不部署;任何 AI 与流水线都不允许 SSH 到服务器执行命令。 +> 部署由 kepo 服务器本地定时任务轮询 Gitea Release 自动完成。 +> 本文档所有操作均由管理员本人在服务器终端执行,不依赖 AI 远程执行。 + +## 架构 + +``` +push main → Gitea Actions 构建 cmd/web + web-commercial 前端 → web-deploy.tar.gz 发布为 web-v* Release + ↓ +kepo 服务器 cron(每分钟)运行 deploy-watch.sh(TAG_PREFIX=web-v)→ 发现新 release → HTTPS 拉取 + ↓ +deploy-web.sh 原子切换 + systemctl restart lunar-web + 健康检查(失败自动回滚) + ↓ +Cloudflare (HTTPS 终结) → kepo 源站 :8080 lunar-web systemd 服务 +``` + +## 与小程序接口部署的差异 + +| | lunar.neatcn.com | kepo.net | +|---|---|---| +| 二进制 | `cmd/mini` | `cmd/web` | +| Release 前缀 | `server-v*` | `web-v*` | +| 部署包 | `deploy.tar.gz` | `web-deploy.tar.gz` | +| 运行方式 | Docker 容器 | systemd 服务 `lunar-web` | +| 部署目录 | `/opt/lunar/production` | `/opt/lunar-web/production` | + +## 一次性安装步骤(按序执行) + +### 1. 准备目录与脚本 + +```bash +mkdir -p /opt/lunar-web/{production,scripts,backups} +# 从仓库复制脚本(任选其一): +# git clone 后 cp .gitea/scripts/{deploy-watch.sh,deploy-web.sh} /opt/lunar-web/scripts/ +# 或直接下载原始文件写入 +chmod +x /opt/lunar-web/scripts/*.sh +``` + +### 2. 生产环境变量 + +写入 `/opt/lunar-web/production/.env`,权限 `600`: + +```ini +SERVER_ENV=production +SERVER_PORT=8080 +PUBLIC_BASE_URL=https://kepo.net +WEB_APP_URL=https://kepo.net +CORS_ORIGINS=https://kepo.net,https://www.kepo.net + +DB_TYPE=mysql +DB_HOST=<数据库地址> +DB_PORT=3306 +DB_USER=<用户名> +DB_PASSWORD=<密码> +DB_NAME=lunar_web # 建议与小程序库分开;若暂时共用,填 lunar + +JWT_SECRET=<强随机值,至少32位> +ADMIN_PASSWORD=<后台登录密码> + +SMTP_HOST=smtp.qq.com +SMTP_PORT=465 +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_FROM_EMAIL=z@kepo.net +SMTP_TLS_MODE=implicit + +STRIPE_SECRET_KEY= +STRIPE_WEBHOOK_SECRET= +BINANCE_PAY_API_KEY= +BINANCE_PAY_SECRET_KEY= +BINANCE_PAY_WEBHOOK_PUBLIC_KEY= +``` + +> kepo 侧不需要 `MINI_APP_ID` / `MINI_APP_SECRET` / `WECHAT_PAY_*`; +> lunar 侧则不需要 SMTP / Stripe / Binance 变量。 + +### 3. systemd 服务 + +`/etc/systemd/system/lunar-web.service`: + +```ini +[Unit] +Description=Lunar Commercial Web (kepo.net) +After=network.target + +[Service] +Type=simple +WorkingDirectory=/opt/lunar-web/production/current +EnvironmentFile=/opt/lunar-web/production/.env +ExecStart=/opt/lunar-web/production/current/bin/server +Restart=always +RestartSec=3 + +[Install] +WantedBy=multi-user.target +``` + +```bash +systemctl daemon-reload +systemctl enable lunar-web +# 首次启动前需要先手工放一版部署包,或等待第一次 Release 拉取 +``` + +### 4. 定时拉取任务 + +crontab(`crontab -e`)添加: + +```cron +* * * * * TAG_PREFIX=web-v ASSET_NAME=web-deploy.tar.gz \ + STATE_FILE=/opt/lunar-web/production/.last-deployed-release \ + DEPLOY_SCRIPT=/opt/lunar-web/scripts/deploy-web.sh \ + LOG_FILE=/opt/lunar-web/deploy-watch.log \ + /opt/lunar-web/scripts/deploy-watch.sh +``` + +首次运行后会自动部署最新 `web-v*` Release。日志:`/opt/lunar-web/deploy-watch.log`。 + +### 5. Cloudflare 回源 + +kepo.net 已走 Cloudflare。确认回源指向本机 8080(或 Nginx 反代到 8080)。 +服务只监听 `127.0.0.1` 或由防火墙限制 8080 仅本机访问。 + +## 验证 + +```bash +curl -s http://127.0.0.1:8080/api/version # 应返回 version/buildTime/commitSha +curl -s -o /dev/null -w '%{http_code}' https://kepo.net/ +curl -s -o /dev/null -w '%{http_code}' https://kepo.net/admin/ +``` + +## 注意 + +- **数据库**:短期内两边共用 `lunar` 库(后台 `/admin` 在 kepo 侧统一查看订单与小程序数据)。后续如需分离,新建 `lunar_web` 库并改 `DB_NAME`。 +- **回滚**:`deploy-web.sh` 健康检查失败会自动恢复 `current.old`;也可手动 `systemctl restart lunar-web`。 +- **小程序端**:部署包结构不变(`deploy/bin/server`),doc79 上的 watcher 无需改动。 diff --git a/.gitea/scripts/deploy-watch.sh b/.gitea/scripts/deploy-watch.sh index 5ba182b..8ac0f93 100644 --- a/.gitea/scripts/deploy-watch.sh +++ b/.gitea/scripts/deploy-watch.sh @@ -13,6 +13,11 @@ set -u GITEA_URL="${GITEA_URL:-https://gitea.neatcn.com}" REPO="${REPO:-gouki/lunar-mini}" +# 同一脚本服务两种部署形态: +# 小程序接口(doc79):默认 server-v* + deploy.tar.gz +# 商业版 Web(kepo):TAG_PREFIX=web-v ASSET_NAME=web-deploy.tar.gz +TAG_PREFIX="${TAG_PREFIX:-server-v}" +ASSET_NAME="${ASSET_NAME:-deploy.tar.gz}" STATE_FILE="${STATE_FILE:-/opt/lunar/production/.last-deployed-release}" DEPLOY_SCRIPT="${DEPLOY_SCRIPT:-/opt/lunar/scripts/deploy.sh}" LOG_FILE="${LOG_FILE:-/opt/lunar/deploy-watch.log}" @@ -28,7 +33,7 @@ if [ -n "${GITEA_DEPLOY_TOKEN:-}" ]; then AUTH_ARGS=(-H "Authorization: token ${GITEA_DEPLOY_TOKEN}") fi -# 查询最新 20 个 release,取 tag 前缀为 server-v 且带 deploy.tar.gz 附件的最新一个 +# 查询最新 20 个 release,取 tag 前缀匹配且带部署包附件的最新一个 RELEASE_JSON=$(curl -fsS --connect-timeout 10 --max-time 30 \ "${AUTH_ARGS[@]}" \ "$GITEA_URL/api/v1/repos/$REPO/releases?limit=20" \ @@ -37,16 +42,18 @@ RELEASE_JSON=$(curl -fsS --connect-timeout 10 --max-time 30 \ exit 1 } -PARSED=$(python3 - "$WORK_DIR/releases.json" <<'PY' -import json, sys +PARSED=$(TAG_PREFIX="$TAG_PREFIX" ASSET_NAME="$ASSET_NAME" python3 - "$WORK_DIR/releases.json" <<'PY' +import json, os, sys +tag_prefix = os.environ["TAG_PREFIX"] +asset_name = os.environ["ASSET_NAME"] releases = json.load(open(sys.argv[1])) for r in releases: - if not r.get("tag_name", "").startswith("server-v"): + if not r.get("tag_name", "").startswith(tag_prefix): continue if r.get("draft") or r.get("prerelease"): continue for a in r.get("assets", []): - if a.get("name") == "deploy.tar.gz": + if a.get("name") == asset_name: print(r["id"]) print(r["tag_name"]) print(a["id"]) diff --git a/.gitea/scripts/deploy-web.sh b/.gitea/scripts/deploy-web.sh new file mode 100644 index 0000000..f2f12ec --- /dev/null +++ b/.gitea/scripts/deploy-web.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# 商业版 Web(kepo.net)自动部署脚本 - systemd 直接运行二进制 +# 设计原则:拉取式部署,只在服务器本地执行,CI 不通过 SSH 触发 +# 使用方法: ./deploy-web.sh +# +# 目录约定: +# /opt/lunar-web/production/current 当前运行版本(systemd WorkingDirectory) +# /opt/lunar-web/production/.env 环境变量(systemd EnvironmentFile) +# /opt/lunar-web/backups 历史版本备份 +# +# 可通过环境变量覆盖:SERVICE_NAME / DEPLOY_DIR / BACKUP_DIR / HEALTH_URL + +set -e + +DEPLOY_PACKAGE="$1" +SERVICE_NAME="${SERVICE_NAME:-lunar-web}" +DEPLOY_DIR="${DEPLOY_DIR:-/opt/lunar-web/production}" +BACKUP_DIR="${BACKUP_DIR:-/opt/lunar-web/backups}" +HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:8080/api/version}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# 回滚:恢复 current.old 并重启服务 +rollback() { + if [ -d "$DEPLOY_DIR/current.old" ]; then + rm -rf "$DEPLOY_DIR/current" + mv "$DEPLOY_DIR/current.old" "$DEPLOY_DIR/current" + systemctl restart "$SERVICE_NAME" || log_error "回滚后服务重启失败,需人工介入" + log_warn "已回滚到上一版本" + else + log_error "无可回滚版本,需人工介入" + fi +} + +if [ ! -f "$DEPLOY_PACKAGE" ]; then + log_error "部署包不存在: $DEPLOY_PACKAGE" + exit 1 +fi + +log_info "开始自动部署(商业版 Web)..." +log_info "部署包: $DEPLOY_PACKAGE" +log_info "systemd 服务: $SERVICE_NAME" +log_info "部署目录: $DEPLOY_DIR" + +mkdir -p "$DEPLOY_DIR" "$BACKUP_DIR" + +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$BACKUP_DIR/backup_${TIMESTAMP}.tar.gz" +if [ -d "$DEPLOY_DIR/current" ]; then + log_info "备份当前版本到: $BACKUP_FILE" + tar -czf "$BACKUP_FILE" -C "$DEPLOY_DIR" current/ || log_warn "备份失败,继续部署" +fi + +# 解压到暂存目录(不碰 current,避免部署空窗) +log_info "解压部署包..." +TEMP_DIR=$(mktemp -d) +tar -xzf "$DEPLOY_PACKAGE" -C "$TEMP_DIR" + +if [ ! -d "$TEMP_DIR/deploy" ]; then + log_error "部署包结构错误:缺少 deploy 目录" + rm -rf "$TEMP_DIR" + exit 1 +fi +if [ ! -f "$TEMP_DIR/deploy/bin/server" ]; then + log_error "部署包缺少 bin/server 二进制" + rm -rf "$TEMP_DIR" + exit 1 +fi +if [ ! -f "$TEMP_DIR/deploy/frontend/index.html" ]; then + log_error "部署包缺少 frontend/index.html(商业前端未构建?)" + rm -rf "$TEMP_DIR" + exit 1 +fi +chmod +x "$TEMP_DIR/deploy/bin/server" + +# 目录级交换,接近原子 +log_info "部署新版本..." +mv "$TEMP_DIR/deploy" "$DEPLOY_DIR/current.new" +rm -rf "$DEPLOY_DIR/current.old" +if [ -d "$DEPLOY_DIR/current" ]; then + mv "$DEPLOY_DIR/current" "$DEPLOY_DIR/current.old" +fi +mv "$DEPLOY_DIR/current.new" "$DEPLOY_DIR/current" +rm -rf "$TEMP_DIR" + +# 重启 systemd 服务 +log_info "重启服务: $SERVICE_NAME" +if systemctl restart "$SERVICE_NAME"; then + log_info "服务重启成功" +else + log_error "服务重启失败,回滚到上一版本" + rollback + exit 1 +fi + +# 健康检查 +log_info "执行健康检查: $HEALTH_URL" +HEALTHY=0 +for i in $(seq 1 10); do + sleep 2 + if curl -fsS --max-time 5 "$HEALTH_URL" > /dev/null 2>&1; then + HEALTHY=1 + break + fi +done + +if [ "$HEALTHY" = "1" ]; then + log_info "健康检查通过" + curl -fsS --max-time 5 "$HEALTH_URL" && echo "" + rm -rf "$DEPLOY_DIR/current.old" +else + log_error "健康检查失败,回滚到上一版本" + journalctl -u "$SERVICE_NAME" --no-pager -n 50 || true + rollback + exit 1 +fi + +# 清理旧备份(保留最近 5 个) +ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm -f + +log_info "部署完成!" +log_info "备份文件: $BACKUP_FILE" +log_info "部署目录: $DEPLOY_DIR/current" diff --git a/.gitea/workflows/server-deploy.yml b/.gitea/workflows/server-deploy.yml index 173597e..ff99635 100644 --- a/.gitea/workflows/server-deploy.yml +++ b/.gitea/workflows/server-deploy.yml @@ -85,11 +85,13 @@ jobs: echo "Building server version: $VERSION" # ldflags 注入版本信息;目标平台为 doc79(linux/amd64) + # 本流水线构建的是小程序接口二进制(cmd/mini); + # 商业版 Web(cmd/web)由 web-deploy.yml 单独构建发布 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 "$LUNAR_GO" build -ldflags "\ -X main.Version=$VERSION \ -X main.BuildTime=$BUILD_TIME \ -X main.CommitSha=$COMMIT_SHORT" \ - -o bin/server cmd/main.go + -o bin/server ./cmd/mini # 版本元数据随包分发,便于服务器侧追溯 cat > bin/version.env </dev/null 2>&1; then + if command -v apk >/dev/null 2>&1; then apk add --no-cache curl + elif command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq curl + elif command -v dnf >/dev/null 2>&1; then dnf install -y -q curl + else echo "无法安装 curl"; exit 1; fi + fi + curl --version | head -1 + + - name: Prepare Go toolchain + run: | + TOOLDIR="$RUNNER_WORKSPACE/.lunar-toolchain" + mkdir -p "$TOOLDIR" + if [ -x "$TOOLDIR/go/bin/go" ]; then + echo "复用缓存 Go 工具链" + else + wget -q --tries=2 --timeout=60 -O "$TOOLDIR/go.tgz" \ + "https://go.dev/dl/go1.22.10.linux-amd64.tar.gz" \ + || wget -q --tries=2 --timeout=60 -O "$TOOLDIR/go.tgz" \ + "https://mirrors.aliyun.com/golang/go1.22.10.linux-amd64.tar.gz" + rm -rf "$TOOLDIR/go" + tar -C "$TOOLDIR" -xzf "$TOOLDIR/go.tgz" + rm -f "$TOOLDIR/go.tgz" + fi + "$TOOLDIR/go/bin/go" version + echo "LUNAR_GO=$TOOLDIR/go/bin/go" >> "$GITEA_ENV" + + - name: Prepare Node toolchain + run: | + TOOLDIR="$RUNNER_WORKSPACE/.lunar-toolchain" + mkdir -p "$TOOLDIR" + if [ -x "$TOOLDIR/node/bin/node" ] && "$TOOLDIR/node/bin/node" --version >/dev/null 2>&1; then + echo "复用缓存 Node 工具链" + else + rm -rf "$TOOLDIR/node" "$TOOLDIR/node.tgz" + mkdir -p "$TOOLDIR" + if ldd --version 2>&1 | grep -qi musl || [ ! -e /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ]; then + NODE_DIST="node-v22.12.0-linux-x64-musl" + NODE_SHA="8d96de0293fc3978d09e2a3fee68c66bca4efcca90f52105e6f7463636e97308" + URLS="https://registry.npmmirror.com/-/binary/node-unofficial-builds/v22.12.0/$NODE_DIST.tar.gz + https://unofficial-builds.nodejs.org/download/release/v22.12.0/$NODE_DIST.tar.gz" + else + NODE_DIST="node-v22.12.0-linux-x64" + NODE_SHA="e05a4d65232ae2b27b3d77da2e368522fb46b923335b8e0d5f77624c32484044" + URLS="https://nodejs.org/dist/v22.12.0/$NODE_DIST.tar.gz + https://mirrors.aliyun.com/nodejs-release/v22.12.0/$NODE_DIST.tar.gz" + fi + DL_OK="" + for url in $URLS; do + echo "尝试下载: $url" + if wget -t 2 -T 120 -q -O "$TOOLDIR/node.tgz" "$url"; then + if [ "$(sha256sum "$TOOLDIR/node.tgz" | cut -d' ' -f1)" = "$NODE_SHA" ]; then + echo "下载成功且校验通过" + DL_OK=1 + break + fi + echo "sha256 校验失败,尝试下一源" + else + echo "wget 下载失败,尝试下一源" + fi + rm -f "$TOOLDIR/node.tgz" + done + test -n "$DL_OK" || { echo "::error::所有下载源均失败或校验不通过"; exit 1; } + tar -C "$TOOLDIR" -xzf "$TOOLDIR/node.tgz" + mv "$TOOLDIR/$NODE_DIST" "$TOOLDIR/node" + rm -f "$TOOLDIR/node.tgz" + if [ "$NODE_DIST" = "node-v22.12.0-linux-x64-musl" ] && [ ! -e /usr/lib/libstdc++.so.6 ] && [ ! -e /lib/libstdc++.so.6 ]; then + if command -v apk >/dev/null 2>&1; then apk add --no-cache libstdc++ libgcc || echo "::warning::libstdc++/libgcc 安装失败"; fi + fi + fi + if ! "$TOOLDIR/node/bin/node" --version; then + echo "::error::Node 二进制无法执行" + rm -rf "$TOOLDIR/node" + exit 1 + fi + echo "PATH=$TOOLDIR/node/bin:$PATH" >> "$GITEA_ENV" + + - name: Install pnpm + run: | + # pnpm 随工具链缓存,避免每次重复下载 + TOOLDIR="$RUNNER_WORKSPACE/.lunar-toolchain" + if [ ! -x "$TOOLDIR/pnpm/bin/pnpm" ]; then + npm install -g --prefix "$TOOLDIR/pnpm" pnpm@9 + fi + echo "PATH=$TOOLDIR/pnpm/bin:$PATH" >> "$GITEA_ENV" + pnpm --version + + - name: Run Go tests + working-directory: server + run: | + export HOME="$RUNNER_WORKSPACE" + "$LUNAR_GO" mod download + "$LUNAR_GO" test ./... + + - name: Build commercial frontend + env: + CI: "true" + run: | + export HOME="$RUNNER_WORKSPACE" + # web/ 工作区:装依赖并构建 @lunar/core(web-commercial 的 file: 依赖) + cd web + pnpm install --frozen-lockfile + pnpm --filter @lunar/core build + cd ../web-commercial + pnpm install --frozen-lockfile + npx tsc -b + npx vite build + test -f dist/index.html + + - name: Build web server binary + working-directory: server + env: + GITEA_SHA: ${{ gitea.sha }} + GITEA_RUN_NUMBER: ${{ gitea.run_number }} + run: | + export HOME="$RUNNER_WORKSPACE" + mkdir -p bin + + VERSION="1.1.${GITEA_RUN_NUMBER}" + BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + COMMIT_SHORT="${GITEA_SHA:0:7}" + + echo "Building web server version: $VERSION" + + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 "$LUNAR_GO" build -ldflags "\ + -X main.Version=$VERSION \ + -X main.BuildTime=$BUILD_TIME \ + -X main.CommitSha=$COMMIT_SHORT" \ + -o bin/server ./cmd/web + + cat > bin/version.env <