# HTTP: validator, RealIP, REST groups, MVC HTTP and TCP share Application services in `internal/module/*`. Gin is a thin adapter. ## MVC | Layer | Lives in | Owns | |-------|----------|------| | Model / application | `internal/module/{biz}/{entity,model,logic}` | Domain types; one GORM file per table; logic by function, not by table | | Controller | `internal/transport/http/handler/{biz}` | Bind, call service, write envelope | | View | `internal/pkg/reply` + `internal/transport/http/respond` | `{code,msg,data}` | Handlers must not contain business rules (credential check, token issue, friend/group permission, persist). Example: login belongs on `auth/logic.LoginService`, not in the Gin handler. ```go func (h *Handler) Login(c *gin.Context) { var in auth.LoginInput if err := respond.BindJSON(c, &in); err != nil { respond.JSONError(c, err) return } tok, err := h.login.Login(c.Request.Context(), &in) if err != nil { respond.JSONError(c, err) return } respond.JSON(c, tok) } ``` Wire services in `internal/bootstrap`, pass them as `httpx.EngineDeps`. TCP commands call the same services. Do not import `internal/transport/http` (package `httpx`) from `handler/*` — that is an import cycle. Use `respond` for Bind/JSON. ## Validator (`github.com/go-playground/validator/v10`) Use `validate` tags. Bind with `respond.BindJSON` (JSON decode + `validate.StructLang`). Do not use Gin's `binding` tags as the source of client messages. `label` and `errmsg` are **i18n keys** (see [i18n.md](i18n.md)), not a single language's sentences: - `label:"field.email"` — field name in default templates (`validate.required` → `{label}不能为空` / `{label} is required`) - `errmsg:"required:validate.login.email_required"` — per-tag key, `;` separates tags Failed validation is `errs.ErrInvalidArgument` (`40001`) with `Msg` already localized. Transport must not replace it. Shared DTO example (`internal/module/auth/logic.LoginInput`): ```go type LoginInput struct { Email string `json:"email" validate:"required,email" label:"field.email" errmsg:"required:validate.login.email_required;email:validate.login.email_invalid"` VerifyCode string `json:"verify_code" validate:"required" label:"field.verify_code" errmsg:"required:validate.login.code_required"` DeviceID uint64 `json:"device_id" validate:"required" label:"field.device_id" errmsg:"required:validate.login.device_required"` } ``` TCP may call `validate.StructLang` / `LoginService` on the same struct. Keep DTOs in the module, not in the handler package. ## RealIP on context (C6) `reqctx.RequestContext.RealIP` is the client address for rate limit and audit. Never use gnet `RemoteAddr` (Nginx → 127.0.0.1). | Transport | How RealIP is set | |-----------|-------------------| | HTTP | `X-Real-IP`, else first `X-Forwarded-For`, else `RemoteAddr` host (`reqctx.ParseHTTPRealIP`) | | TCP | PROXY protocol on the session, copied in `session.RequestContext()` | Read it with `reqctx.RealIP(ctx)`. Trace middleware must attach `*reqctx.RequestContext` before Auth and handlers. Zap fields already include `real_ip` via `logger.Attach`. Gin binds loopback only (C4); Nginx on 443 sets `X-Real-IP`. `SetTrustedProxies` is `127.0.0.1` / `::1`. ## Language on context Every HTTP request carries `X-Language` or `Accept-Language`. TCP AUTH carries `language`. Canonical values: `zh-CN` (default), `en`, `zh-TW`. Details: [i18n.md](i18n.md). ## REST grouping Public HTTP is `/v1/{resource}/...`. Register with Gin groups in `internal/transport/http/router.go`, one function per business. | Group | Prefix | Examples | |-------|--------|----------| | system | `/healthz`, `/readyz`, `/docs` | liveness, readiness, OpenAPI | | auth | `/v1/auth` | `POST /v1/auth/login`, `POST /v1/auth/email-codes`, `POST /v1/auth/token/refresh`, `POST /v1/auth/logout` | | users | `/v1/users` | `GET/PATCH /v1/users/me` | | friends | `/v1/friends` | apply, requests, accept, profile, remark, block, report | | groups | `/v1/groups` | create, profile, members, invite, kick, leave, transfer, dissolve | | conversations | `/v1/conversations` | list, mute | | messages | `/v1/messages` | history (sync still prefers TCP) | | files | `/v1/files` | upload credential | Rules: - Noun resources, HTTP verbs for actions. No `/getUser`, `/sendMessage` under HTTP. - Login stays `POST /v1/auth/login` (create session). Do not put login under `/v1/users`. - Anonymous paths belong in `publicPath` (login, email-codes, token refresh, healthz, readyz, docs). - OpenAPI `tags` must match these groups. ## Checklist when adding an HTTP API 1. Input struct in `internal/module/{biz}` with `validate` / `label` / `errmsg` **keys**. 2. Use-case method on a service in that module (HTTP and TCP share it). 3. Thin handler under `internal/transport/http/handler/{biz}`. 4. Register on the matching `v1.Group` in `router.go`. 5. Document the path and tag in `internal/transport/http/docs/openapi.yaml`. 6. Assert `reqctx.RealIP(ctx)` wherever you rate-limit or audit. 7. Localize errors/notifications via `internal/pkg/i18n`; never hardcode `msg`.