refactor: serve static frontend with Go APIs
This commit is contained in:
1 parent
763d2f0648
commit
b14b4fced7
101 files changed
+963
-3928
No files matched your search
+13
-13
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user