docs: 完善 README 目录结构与架构说明
This commit is contained in:
1 parent
d98a78a1f7
commit
4611e8bf9d
1 file changed
+164
-16
@@ -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/<name>/` 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/<module>/`.
|
||||
|
||||
### 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 `<base href="__DJ_ADMIN_BASE__/">`
|
||||
placeholder, which the server rewrites on startup. Consequences for admin code:
|
||||
|
||||
- native `<a href>` and `window.location` navigation must go through `adminUrl()` in
|
||||
`src/utils/adminBase.ts`
|
||||
- `<router-link :to>` 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/<name>/` 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 <base> 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`
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=dujiao-next/dujiao-next&type=date&legend=top-left&sealed_token=pLO1UK6ooAVrG-Ax2T2YaXxp2jAmvLNEOCMtlLr3tVrDSS1GHTeQIEjhMpafFToiXGjdEOkjTK4QERxqQjl8-xjwmo4ngQqOwxBZpzcVfqpF6braIFEhJRM1iAVRA7wbrUAQltZSRwebK_w0CUDg-cChnGbROE1WTSted0VXWtKg28dhOY9-GCn7KXsH" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=dujiao-next/dujiao-next&type=date&legend=top-left&sealed_token=pLO1UK6ooAVrG-Ax2T2YaXxp2jAmvLNEOCMtlLr3tVrDSS1GHTeQIEjhMpafFToiXGjdEOkjTK4QERxqQjl8-xjwmo4ngQqOwxBZpzcVfqpF6braIFEhJRM1iAVRA7wbrUAQltZSRwebK_w0CUDg-cChnGbROE1WTSted0VXWtKg28dhOY9-GCn7KXsH" />
|
||||
</picture>
|
||||
</a>
|
||||
</a>
|
||||
Reference in new issue
Block a user