spark-server/.cursor/skills/spark-im-v3/http.md
2026-09-18 14:50:41 +08:00

108 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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