From 8842cabe7817ede28c1341c053847c22d842a6bc Mon Sep 17 00:00:00 2001 From: dujiao-next Date: Sun, 26 Jul 2026 15:56:09 +0800 Subject: [PATCH] build: ship a single fullstack binary and docker image --- .dockerignore | 20 +++++++++ .github/workflows/ci.yml | 56 ++++++++++++++++++++++-- .github/workflows/release.yml | 44 +++---------------- .gitignore | 14 +++--- .goreleaser.yaml | 58 +++++++++---------------- Dockerfile | 42 ++++++++++++++++-- README.md | 81 +++++++++++++++++++++++++++-------- 7 files changed, 207 insertions(+), 108 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..654ebeef --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.git +.github +.idea +.vscode +.DS_Store + +# 运行时数据,不属于构建上下文 +db/ +uploads/ +logs/ +config.yml +*.db +*.log + +# 构建产物:前端 dist 由镜像内的 frontend 阶段重新生成, +# internal/web/dist 由 COPY --from=frontend 填充,都不能从宿主机带进来 +dist/ +frontend/*/dist/ +frontend/*/node_modules/ +internal/web/dist/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 972b32db..fba05a86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,13 +15,13 @@ concurrency: cancel-in-progress: true jobs: - verify: + api: name: Verify API runs-on: ubuntu-latest timeout-minutes: 30 steps: - - name: Checkout api + - name: Checkout uses: actions/checkout@v4 - name: Set up Go @@ -33,10 +33,11 @@ jobs: - name: Download dependencies run: go mod download + # 用 git ls-files 限定范围,避免 gofmt 递归遍历 frontend/*/node_modules - name: Check formatting shell: bash run: | - unformatted="$(gofmt -l .)" + unformatted="$(gofmt -l $(git ls-files '*.go'))" if [[ -n "$unformatted" ]]; then echo "::error::The following Go files are not formatted:" printf '%s\n' "$unformatted" @@ -51,3 +52,52 @@ jobs: - name: Build server run: go build ./cmd/server + + fullstack: + name: Verify fullstack build + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.34.3 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '24.11.1' + + # 用子 shell cd 而非 `pnpm --dir`:后者不会读取子目录的 packageManager 字段, + # 在启用 corepack 的环境下会用错 pnpm 版本 + - name: Install frontend dependencies + run: | + (cd frontend/admin && pnpm install --frozen-lockfile) + (cd frontend/user && pnpm install --frozen-lockfile) + + # 两个 build 脚本都以 `vue-tsc -b` 开头,因此这一步同时覆盖了类型检查 + - name: Build frontends + run: | + (cd frontend/admin && pnpm run build:fullstack) + (cd frontend/user && pnpm run build) + + - name: Embed frontend assets + run: | + mkdir -p internal/web/dist + cp -r frontend/admin/dist internal/web/dist/admin + cp -r frontend/user/dist internal/web/dist/user + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + # 只有带 fullstack tag 才会编译 embed_fullstack.go, + # 默认构建覆盖不到,必须在这里单独验证一次完整的嵌入链路。 + - name: Build fullstack binary + run: go build -tags release,fullstack ./cmd/server diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 496a3c91..da8e688f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,28 +12,14 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - name: Checkout api + - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - # ---- fullstack 模式所需:拉取同 tag 的 admin/user 源码并构建 ---- - # admin/user 仓库为 public,无需 PAT。要求三仓库已用 scripts/tag-and-push.sh - # 打过同名 tag。 - - name: Checkout admin (cross-repo) - uses: actions/checkout@v4 - with: - repository: dujiao-next/admin - ref: ${{ github.ref_name }} - path: admin-src - - - name: Checkout user (cross-repo) - uses: actions/checkout@v4 - with: - repository: dujiao-next/user - ref: ${{ github.ref_name }} - path: user-src - + # 前端源码已在本仓库 frontend/ 下,不再需要跨仓库 checkout。 + # 实际的前端构建与嵌入由 .goreleaser.yaml 的 before hooks 完成, + # 这里只负责把 pnpm / Node 工具链准备好。 - name: Setup pnpm uses: pnpm/action-setup@v4 with: @@ -44,30 +30,10 @@ jobs: with: node-version: '24.11.1' - - name: Build admin (fullstack mode) - working-directory: admin-src - run: | - pnpm install --frozen-lockfile - pnpm run build:fullstack - - - name: Build user - working-directory: user-src - run: | - pnpm install --frozen-lockfile - pnpm run build - - - name: Embed frontend assets into api - run: | - mkdir -p internal/web/dist - cp -r admin-src/dist internal/web/dist/admin - cp -r user-src/dist internal/web/dist/user - ls -la internal/web/dist/admin internal/web/dist/user | head -10 - - # ---- Go 工具链 + GoReleaser ---- - name: Set up Go uses: actions/setup-go@v5 with: - go-version: stable + go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 diff --git a/.gitignore b/.gitignore index 9586b112..00b1457b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,14 +7,14 @@ uploads *.log .env .DS_Store -*/node_modules/ -*/dist/ +node_modules/ # Added by goreleaser init: dist/ -# Fullstack 构建产物(仅 -tags fullstack 时使用,不进 git 历史) -internal/web/dist/ +# 前端构建产物与依赖(frontend/{admin,user}) +frontend/*/dist/ +frontend/*/node_modules/ +frontend/*/*.local -# CI workflow 跨仓库 checkout 的前端源码(fullstack 构建用,不进 git 历史) -admin-src/ -user-src/ +# Fullstack 嵌入产物:由前端构建拷贝而来,不进 git 历史 +internal/web/dist/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ce0db50e..a6dc05f7 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,12 +2,29 @@ version: 2 project_name: dujiao-next +# 前端在这里构建并拷进 internal/web/dist/,随后由 -tags=fullstack 的 go:embed 打进二进制。 +# 放在 before hooks 而不是 CI step 里,是为了让本地 `goreleaser build --snapshot` +# 与 GitHub Actions 走完全相同的路径。执行前需要有 pnpm(见 .github/workflows/release.yml)。 before: hooks: - go mod tidy + - cmd: pnpm install --frozen-lockfile + dir: frontend/admin + - cmd: pnpm install --frozen-lockfile + dir: frontend/user + # admin 用 fullstack 模式构建:base 为 './' 且注入 __DJ_ADMIN_BASE__ 占位符, + # 后端启动时按 web.admin_path 替换,使产物可挂载到任意自定义前缀。 + - cmd: pnpm run build:fullstack + dir: frontend/admin + - cmd: pnpm run build + dir: frontend/user + - sh -c 'rm -rf internal/web/dist && mkdir -p internal/web/dist' + - sh -c 'cp -r frontend/admin/dist internal/web/dist/admin' + - sh -c 'cp -r frontend/user/dist internal/web/dist/user' builds: - - id: api + # 单一产物:内嵌 admin + user SPA,一个二进制跑完整服务。 + - id: dujiao-next env: - CGO_ENABLED=0 main: ./cmd/server @@ -15,7 +32,7 @@ builds: ldflags: - -s -w -X github.com/dujiao-next/internal/version.Version={{.Tag}} flags: - - "-tags=release" + - "-tags=release,fullstack" goos: - linux - windows @@ -24,28 +41,10 @@ builds: - amd64 - arm64 - # fullstack 二进制:embed admin + user SPA。仅 linux(小白单二进制部署目标)。 - # CI workflow 在 GoReleaser 之前 checkout admin/user 同 tag 源码、跑前端 build、 - # 把 dist 拷到 internal/web/dist/{admin,user},再由 -tags=fullstack 触发 embed。 - - id: fullstack - env: - - CGO_ENABLED=0 - main: ./cmd/server - binary: dujiao-server - ldflags: - - -s -w -X github.com/dujiao-next/internal/version.Version={{.Tag}} - flags: - - "-tags=release,fullstack" - goos: - - linux - goarch: - - amd64 - - arm64 - archives: - - id: api + - id: dujiao-next ids: - - api + - dujiao-next formats: [tar.gz] files: - ./config.yml.example @@ -62,21 +61,6 @@ archives: - goos: windows formats: [zip] - - id: fullstack - ids: - - fullstack - formats: [tar.gz] - files: - - ./config.yml.example - - ./README.md - name_template: >- - dujiao-all_ - {{- .Tag }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - changelog: sort: asc groups: diff --git a/Dockerfile b/Dockerfile index f9aca7d7..1751e038 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,34 @@ # syntax=docker/dockerfile:1 +# +# 单镜像全栈构建:admin + user 两个 SPA 编译后由 go:embed 打进同一个二进制, +# 由同一个进程在同一端口(8080)提供服务。不再需要独立的前端容器与 nginx。 -FROM golang:1.26.3-alpine AS builder +# ---- 阶段 1:构建两个前端 ---- +# 固定在构建机原生架构上跑(前端产物与目标架构无关),避免 buildx 多平台构建时 +# 在 QEMU 模拟的 arm64 下重跑一遍 Node —— 那样构建耗时会差一个数量级。 +FROM --platform=$BUILDPLATFORM node:24.11.1-alpine AS frontend + +WORKDIR /src + +ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +RUN corepack enable + +# 先只拷 manifest,让依赖安装层能在源码变动时命中缓存 +COPY frontend/admin/package.json frontend/admin/pnpm-lock.yaml ./admin/ +COPY frontend/user/package.json frontend/user/pnpm-lock.yaml ./user/ +RUN cd admin && pnpm install --frozen-lockfile +RUN cd user && pnpm install --frozen-lockfile + +COPY frontend/admin ./admin +COPY frontend/user ./user + +# admin 走 fullstack 模式:注入 __DJ_ADMIN_BASE__ 占位符, +# 后端启动时按 web.admin_path 替换,使同一份产物能挂到任意自定义前缀。 +RUN cd admin && pnpm run build:fullstack +RUN cd user && pnpm run build + +# ---- 阶段 2:构建内嵌前端的 Go 二进制 ---- +FROM --platform=$BUILDPLATFORM golang:1.26.3-alpine AS builder ARG TARGETOS ARG TARGETARCH @@ -16,12 +44,18 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . + +# go:embed 只能读取包目录内的文件,所以前端产物必须落到 internal/web/dist/ 下 +COPY --from=frontend /src/admin/dist ./internal/web/dist/admin +COPY --from=frontend /src/user/dist ./internal/web/dist/user + RUN set -eux; \ export GOOS="$TARGETOS" GOARCH="$TARGETARCH"; \ if [ "$TARGETARCH" = "arm" ] && [ -n "$TARGETVARIANT" ]; then export GOARM="${TARGETVARIANT#v}"; fi; \ if [ "$TARGETARCH" = "amd64" ] && [ -n "$TARGETVARIANT" ]; then export GOAMD64="${TARGETVARIANT#v}"; fi; \ - go build -trimpath -tags release -ldflags="-s -w -X github.com/dujiao-next/internal/version.Version=${APP_VERSION}" -o /out/dujiao-api ./cmd/server + go build -trimpath -tags release,fullstack -ldflags="-s -w -X github.com/dujiao-next/internal/version.Version=${APP_VERSION}" -o /out/dujiao-next ./cmd/server +# ---- 阶段 3:运行时 ---- FROM alpine:latest WORKDIR /app @@ -29,9 +63,9 @@ WORKDIR /app RUN apk --no-cache add ca-certificates tzdata \ && mkdir -p /app/db /app/uploads /app/logs -COPY --from=builder /out/dujiao-api /app/dujiao-api +COPY --from=builder /out/dujiao-next /app/dujiao-next COPY config.yml.example /app/config.yml.example EXPOSE 8080 -CMD ["./dujiao-api"] +CMD ["./dujiao-next"] diff --git a/README.md b/README.md index ae6dec00..8bd7e3b9 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,75 @@ -# Dujiao-Next API +# Dujiao-Next -Dujiao-Next API is the backend service for the Dujiao-Next ecosystem. It provides public APIs, user/auth APIs, order and payment workflows, and admin APIs. +Dujiao-Next is a digital goods e-commerce platform. This repository contains the complete +application: the Go backend, the customer storefront, and the admin panel. + +**One binary runs everything.** Both frontends are compiled into the server binary with +`go:embed`, so a production deployment is a single process on a single port behind a single +domain — no separate frontend containers, no nginx serving static files. ## Tech Stack -- Go -- Gin -- GORM -- SQLite / PostgreSQL +- Go · Gin · GORM · SQLite / PostgreSQL +- Vue 3 · Vite · TypeScript · Tailwind CSS v4 -## What This Service Does +## Repository Layout -- Serves REST APIs for user, order, and payment flows -- Handles payment callbacks/webhooks -- Supports product, fulfillment, and configuration management - -## Quick Start - -```bash -go mod tidy -go run cmd/server/main.go +``` +├── cmd/server/ # entry point +├── internal/ +│ └── web/ # frontend embedding and SPA route mounting +├── frontend/ +│ ├── admin/ # admin panel +│ └── user/ # customer storefront +├── config.yml.example +├── Dockerfile # single full-stack image +└── .goreleaser.yaml ``` -The default health check endpoint is: +## Quick Start (Deploy) -- `GET /health` +Download the latest `dujiao-next_*.tar.gz` from [Releases](https://github.com/dujiao-next/dujiao-next/releases): + +```bash +tar -xzf dujiao-next_*.tar.gz +cp config.yml.example config.yml +# edit config.yml: set jwt.secret, user_jwt.secret, and web.admin_path +./dujiao-next +``` + +The storefront is served at `/` and the admin panel at `web.admin_path` (default `/admin` — +change it). Full instructions: https://dujiao-next.com/deploy/ + +Or with Docker: + +```bash +docker run -d -p 8080:8080 -v $PWD/config.yml:/app/config.yml:ro dujiaonext/api:latest +``` + +## Quick Start (Develop) + +Run the backend and the two frontends separately for hot reload: + +```bash +go mod tidy && go run ./cmd/server # :8080 — API only, no SPAs mounted + +cd frontend/user && pnpm install && pnpm run dev # :5173 +cd frontend/admin && pnpm install && pnpm run dev # :5174 +``` + +Both dev servers proxy `/api` and `/uploads` to `localhost:8080`. + +## Building the Full-Stack Binary + +```bash +goreleaser build --snapshot --single-target --clean +``` + +This builds both frontends, embeds them, and compiles with `-tags fullstack` — the same path +CI uses for releases. See [Manual Deployment](https://dujiao-next.com/deploy/manual) for the +step-by-step equivalent. + +Health check endpoint: `GET /health` ## Online Documentation