refactor: serve static frontend with Go APIs

This commit is contained in:
brother7 committed 2026-08-16 20:47:45 +08:00
1 parent 763d2f0648
commit b14b4fced7
101 files changed
+963 -3928

No files matched your search

+13 -13
View File
@@ -1,10 +1,10 @@
# Go modular backend
This directory contains the separately runnable Go implementation of ADR-003.
It now owns the checked-in compatibility implementation for all 66 explicit
HTTP method/path entries, but it is **not** the current production traffic
owner: Next.js, the Node Worker, Docker Compose, and the ACK manifests remain
unchanged until a later, explicit cutover.
This directory contains the production application backend. Go is the sole
runtime owner of `/api`, `/uploads`, and `/generated-results`, including browser
sessions, authorization, persistence, provider calls, and the embedded
WorkerLoop. The frontend is a static Next.js export served by Nginx and calls
these Go routes through the public same-origin Ingress.
Implemented Modules:
@@ -76,8 +76,8 @@ docker build -f backend/Dockerfile.runtime \
```
The runner defaults to `CGO_ENABLED=0` for reproducible cross-platform builds.
To exercise the local foundation manually without changing the existing Next
server, use a different port:
To exercise the backend locally, listen on port 8080 and route browser API
requests there with a same-origin development proxy:
```bash
ZHINIAN_DATA_BACKEND=local GO_BACKEND_PORT=8080 ./backend/zhinian-api
@@ -85,8 +85,7 @@ ZHINIAN_DATA_BACKEND=local GO_BACKEND_PORT=8080 ./backend/zhinian-api
In local mode, persistent business data is process-local and is discarded on
restart; it is intended only for development and contract smoke tests. The
default demo identity is the same optional-auth super administrator used by the
current Next development flow.
default demo identity supports the optional-auth frontend development flow.
## First super administrator
@@ -105,7 +104,8 @@ process startup so a misconfigured bootstrap is visible instead of silently
missing. Local development mode keeps the seeded demo administrator and never
bootstrap-creates accounts.
No Ingress, Docker, ACK, Secret, or Worker ownership has moved to Go yet, so
Next.js remains the deployed owner of every route. Real RDS/CA, OSS, provider,
Webhook, Worker drain/recovery, and rollback validation are mandatory before
that route ownership changes.
Production routing, Secret ownership, probes, and rollout commands are defined
in [`../docs/DEPLOYMENT.md`](../docs/DEPLOYMENT.md) and `../deploy/ack/`. Go owns
the session signing Secret and backend runtime configuration; the static Web
workload receives neither. Validate RDS/CA, OSS, providers, Webhooks, embedded
Worker recovery, and rollback behavior for each production release.
@@ -93,7 +93,7 @@ func TestProductionLocalBackendNeverGrantsAnonymousAdministrator(t *testing.T) {
}
}
func TestApplicationDerivesNextCompatibleMethodMatrixBeforeAuthentication(t *testing.T) {
func TestApplicationDerivesLegacyCompatibleMethodMatrixBeforeAuthentication(t *testing.T) {
app, err := application.New(application.Options{Getenv: applicationEnv(map[string]string{
"NODE_ENV": "production",
"ZHINIAN_DATA_BACKEND": "local",
+2 -2
View File
@@ -92,8 +92,8 @@ type AdjustmentCommand struct {
AmountFen, DeltaFen int64
}
type AdjustmentResult struct {
Wallet Wallet
Entry LedgerEntry
Wallet Wallet `json:"wallet"`
Entry LedgerEntry `json:"entry"`
}
type AccountConfigStore interface {
+18 -1
View File
@@ -74,9 +74,26 @@ func TestBillingAdminRoutesRequireSuperAdminAndValidateWrites(t *testing.T) {
if got := serveJSON(t, super, http.MethodPatch, "/api/admin/billing/account", map[string]any{"accountName": " Acme ", "bankName": " Bank ", "accountNumber": " 123 ", "contact": " Ops "}); got.Code != 200 || account.saved.AccountName != "Acme" {
t.Fatalf("account status=%d saved=%+v body=%s", got.Code, account.saved, got.Body.String())
}
if got := serveJSON(t, super, http.MethodPost, "/api/admin/billing/adjustments", map[string]any{"organizationId": "org", "amountYuan": 1.235, "direction": "debit", "note": " correction "}); got.Code != 200 || service.adjustment.AmountFen != 124 || service.adjustment.DeltaFen != -124 || service.adjustment.OperatorID != "root" {
got := serveJSON(t, super, http.MethodPost, "/api/admin/billing/adjustments", map[string]any{"organizationId": "org", "amountYuan": 1.235, "direction": "debit", "note": " correction "})
if got.Code != 200 || service.adjustment.AmountFen != 124 || service.adjustment.DeltaFen != -124 || service.adjustment.OperatorID != "root" {
t.Fatalf("adjustment status=%d got=%+v body=%s", got.Code, service.adjustment, got.Body.String())
}
var adjustmentResponse map[string]json.RawMessage
if err := json.Unmarshal(got.Body.Bytes(), &adjustmentResponse); err != nil {
t.Fatalf("decode adjustment response: %v", err)
}
if _, ok := adjustmentResponse["wallet"]; !ok {
t.Fatalf("adjustment response missing lowercase wallet: %s", got.Body.String())
}
if _, ok := adjustmentResponse["entry"]; !ok {
t.Fatalf("adjustment response missing lowercase entry: %s", got.Body.String())
}
if _, ok := adjustmentResponse["Wallet"]; ok {
t.Fatalf("adjustment response contains uppercase Wallet: %s", got.Body.String())
}
if _, ok := adjustmentResponse["Entry"]; ok {
t.Fatalf("adjustment response contains uppercase Entry: %s", got.Body.String())
}
if got := serveJSON(t, super, http.MethodPatch, "/api/admin/billing/prices/price-1", map[string]any{"standardUnitPriceFen": 1, "markupMultiplier": 2}); got.Code != 400 {
t.Fatalf("whitelist status=%d", got.Code)
}
+4 -4
View File
@@ -12,10 +12,10 @@ type routeMethodPattern struct {
allow []string
}
// WithRouteMethodCompatibility applies the method behavior that Next derives
// from app route exports. It sits outside authentication so OPTIONS never
// depends on runtime configuration and HEAD executes the corresponding GET
// semantics while suppressing the response body.
// WithRouteMethodCompatibility applies the legacy-compatible method matrix
// for the checked-in public HTTP contract. It sits outside authentication so
// OPTIONS never depends on runtime configuration and HEAD executes the
// corresponding GET semantics while suppressing the response body.
func WithRouteMethodCompatibility(next http.Handler) http.Handler {
patterns := routeMethodPatterns(GoRouteSurface())
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
// RouteSurface describes one externally visible route explicitly owned by the
// Go modular monolith. It is kept source-independent so contract tests can
// compare it with the checked-in Next.js surface before any cutover.
// compare it with the checked-in public HTTP contract.
type RouteSurface struct {
Method string `json:"method"`
Path string `json:"path"`
@@ -10,7 +10,7 @@ import (
"testing"
)
func TestGoRouteSurfaceExactlyMatchesCheckedInNextManifest(t *testing.T) {
func TestGoRouteSurfaceExactlyMatchesCheckedInPublicHTTPContract(t *testing.T) {
_, source, _, _ := runtime.Caller(0)
raw, err := os.ReadFile(filepath.Join(filepath.Dir(source), "../../../contracts/http/route-surface.v1.json"))
if err != nil {