Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout. Co-authored-by: Cursor <cursoragent@cursor.com>
106 lines
5.1 KiB
Markdown
106 lines
5.1 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,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`.
|