108 lines
5.4 KiB
Markdown
108 lines
5.4 KiB
Markdown
# 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,request_id,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(`/docs` API,`/docs/enums` 枚举,`/docs/errors` 错误码) |
|
||
| 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, pin, 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 |
|
||
| meta | `/v1/meta` | `GET /v1/meta/enums`(无需登录) |
|
||
|
||
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, `GET /v1/meta/enums`).
|
||
- OpenAPI `tags` must match these groups. Docs jump tags `enums` / `errors` sit above `system` and open `/docs/enums`、`/docs/errors`.
|
||
|
||
## 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`.
|
||
8. Closed type/status: entity typed enum + OpenAPI named schema `$ref` ([enums.md](enums.md)). Do not inline a second `enum:` list.
|