spark-server/.cursor/skills/spark-im-v3/http.md
zhr ae9a71034d feat: implement Spark IM V3 modular monolith
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>
2026-09-17 19:52:11 +08:00

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`.