diff --git a/README.md b/README.md index 8bd7e3b9..eaf23bf1 100644 --- a/README.md +++ b/README.md @@ -3,29 +3,141 @@ 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 -- Vue 3 · Vite · TypeScript · Tailwind CSS v4 +| Layer | Stack | +| --- | --- | +| Backend | Go 1.26 · Gin · GORM · SQLite / PostgreSQL | +| Auth | JWT (separate admin / user realms) · Casbin RBAC · TOTP 2FA | +| Async | asynq on Redis (optional — the server runs without it) | +| Config | Viper (`config.yml`) | +| Frontend | Vue 3 · Vite · TypeScript · Tailwind CSS v4 · pnpm 10 | +| Admin UI | shadcn-vue / reka-ui | ## Repository Layout ``` -├── cmd/server/ # entry point +. +├── cmd/server/ # entry point; also hosts the `admin` operator subcommands ├── internal/ -│ └── web/ # frontend embedding and SPA route mounting +│ ├── app/ # composition root +│ │ ├── container/ # dependency-injection container +│ │ ├── httpserver/ # Gin router, route groups, middleware +│ │ └── jobs/ # asynq worker service and consumers +│ ├── bootstrap/ # per-module wiring (adapters.go + wiring.go) +│ ├── modules/ # 35 business modules — one vertical slice per domain +│ ├── workflows/ # use cases that span several modules +│ ├── platform/ # framework-facing infrastructure +│ │ ├── database/gormdb/ # connection, auto-migration +│ │ └── http/ # response envelope, Gin helpers +│ ├── shared/ # dependency-free primitives (money, jsonmap, serial …) +│ ├── authz/ # Casbin RBAC: policy model, built-in role seeds +│ ├── web/ # SPA embedding and mounting (build-tag gated) +│ ├── architecture/ # architecture guard tests — no production code +│ ├── cache/ config/ constants/ crypto/ i18n/ logger/ queue/ version/ +│ └── admincmd/ htmltext/ persistence/ telegramidentity/ testkit/ upstream/ ├── frontend/ -│ ├── admin/ # admin panel -│ └── user/ # customer storefront +│ ├── admin/ # admin panel SPA (dev :5174) +│ └── user/ # customer storefront SPA (dev :5173) ├── config.yml.example -├── Dockerfile # single full-stack image +├── Dockerfile # single full-stack image └── .goreleaser.yaml ``` +Runtime directories created on first start: `db/` (SQLite), `uploads/`, `logs/`. + +## Architecture + +A modular monolith. Each domain under `internal/modules//` is a vertical slice with its +own layers: + +| Layer | Holds | May import | +| --- | --- | --- | +| `domain/` | entities, value objects, business invariants | nothing from the other layers | +| `application/` | use cases, port interfaces | `domain`, `contract` | +| `infrastructure/` | GORM stores, gateways, queue adapters | `domain`, `application` ports | +| `transport/` | HTTP handlers, presenters | `application` contracts | +| `contract/` | port interfaces the application layer depends on, and the module's public surface for other modules | — | + +**These rules are enforced by tests, not convention.** `internal/architecture/` parses every +import in the tree and fails the build on violations. The main ones: + +- `domain` must not reach into `application`, `infrastructure`, or `transport` +- `application` must not import Gin or asynq — no transport libraries in use cases +- only a module's `infrastructure/gormstore` adapter may import GORM +- `transport` depends on application contracts, never on concrete stores +- `internal/shared` stays free of modules, GORM, Gin, and asynq +- `internal/platform` must not depend on business modules + +Run them with the rest of the suite: `go test ./internal/architecture/...` + +Modules never import each other's internals — they talk through `contract/`, and the wiring +lives in `internal/bootstrap//`. + +### RBAC + +Every `/api/v1/admin/...` route passes through Casbin. The permission catalog is generated +from the live route table, but the **built-in roles are hand-maintained** in +`internal/authz/bootstrap.go`. Adding an admin route without adding it to a role seed leaves +that route reachable only by the super admin. `internal/app/httpserver/rbac_coverage_test.go` +checks that every registered route is covered. + +## Build Tags + +| Tag | Effect | +| --- | --- | +| *(none)* | API only. No SPAs mounted — the default for local development. | +| `fullstack` | Embeds `internal/web/dist/{admin,user}` into the binary via `go:embed`. | +| `release` | Production behavior for outbound URL building. | + +`go:embed all:dist/admin all:dist/user` requires **both** directories to exist, so a +`fullstack` build fails outright if the frontends were not built first. A plain `go build` +does not compile `embed_fullstack.go` — after touching `internal/web/`, verify with +`go build -tags release,fullstack ./cmd/server`. + +## Run Modes + +```bash +./dujiao-next # all — HTTP server + background worker (default) +./dujiao-next -mode api # HTTP server only +./dujiao-next -mode worker # background worker only +``` + +Operator subcommands ship in the same binary, so a container needs no extra tooling: + +```bash +./dujiao-next admin list-admins +./dujiao-next admin reset-password +./dujiao-next admin reset-2fa +``` + +## Frontend Notes + +Two independent SPAs, both built with Vite and embedded at release time. + +**Mount points.** The storefront is served at `/`; the admin panel at `web.admin_path` +(default `/admin`). `/api`, `/uploads`, and `/health` are reserved prefixes — an unmatched +path under them returns 404 instead of falling through to the SPA shell. Adding a new +top-level backend prefix means updating `reservedPaths` in `internal/web/handler.go`. + +**The admin base path is resolved at runtime, not at build time.** Since `web.admin_path` is +configurable, `pnpm run build:fullstack` only injects a `` +placeholder, which the server rewrites on startup. Consequences for admin code: + +- native `` and `window.location` navigation must go through `adminUrl()` in + `src/utils/adminBase.ts` +- `` and `router.push()` must **not** — vue-router already carries the base, + and prefixing again yields `/admin/admin/...` + +**Storefront templates.** The customer frontend ships more than one look, selected by the +`storefront_template` site setting (`classic`, `vault`). Template pages live in +`src/templates//` and fall back to `src/views/` when a page has no template-specific +version; see `src/templates/registry.ts`. Append `?template=vault` to preview one locally. + +**i18n.** Both frontends and all API responses are localized — Simplified Chinese, Traditional +Chinese, and English. Do not hard-code user-facing strings on either side. + ## Quick Start (Deploy) Download the latest `dujiao-next_*.tar.gz` from [Releases](https://github.com/dujiao-next/dujiao-next/releases): @@ -37,8 +149,7 @@ cp config.yml.example config.yml ./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/ +Full instructions: https://dujiao-next.com/deploy/ Or with Docker: @@ -57,7 +168,12 @@ 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`. +Both dev servers proxy `/api`, `/uploads`, `/sitemap.xml`, and `/robots.txt` to +`localhost:8080`. In production everything is same-origin, so these proxies are a +development-only concern. + +> Use `pnpm` via corepack. `pnpm --dir X` does not read the `packageManager` field of the +> target directory and will pick the wrong version — `cd` into the package first. ## Building the Full-Stack Binary @@ -66,11 +182,43 @@ 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. +CI uses for releases. The manual equivalent: + +```bash +(cd frontend/admin && pnpm run build:fullstack) # injects the placeholder +(cd frontend/user && pnpm run build) +rm -rf internal/web/dist && mkdir -p internal/web/dist +cp -r frontend/admin/dist internal/web/dist/admin +cp -r frontend/user/dist internal/web/dist/user +go build -tags release,fullstack -o dujiao-next ./cmd/server +``` + +Note that admin uses `build:fullstack`, not `build`. Plain `build` produces a bundle pinned to +`/`, which silently breaks a custom `web.admin_path`. + +## Testing + +```bash +go test ./... # full suite +go test ./internal/architecture/... # dependency and layering guards +go test ./internal/modules/order/... # one module + +cd frontend/user && pnpm run build # includes vue-tsc type checking +cd frontend/admin && pnpm run build +``` Health check endpoint: `GET /health` +## Notes on Data Access + +SQLite runs with `MaxOpenConns=1`. A store opens a transaction through +`WithinTransaction(func(tx contract.Transaction) error)`, and every query inside the closure +must go through that `tx` handle or a store bound to it via `WithTx(tx)`. Reaching for the +global DB handle instead asks for a second connection that will never be granted, deadlocking +the process — including indirectly, by calling a service that queries on its own. Read any +settings you need *before* opening the transaction, and keep outbound HTTP calls (payment +gateways and the like) outside it. + ## Online Documentation - https://dujiao-next.com @@ -83,4 +231,4 @@ Health check endpoint: `GET /health` Star History Chart - \ No newline at end of file +