spark-server/.cursor/skills/spark-im-v3/http.md
zhr 8870c59e2d docs(cursor): 优化 spark-im-v3 skill 与 Agent 说明
新增 mysql.md(t_ 表名与 migrate 流程),补全 /v1/music·square 与 admin 路由说明;description 与 AGENTS/rules 对齐 make test→lint 与 deploy 门禁,减少与 verify-before-done 重复。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 11:09:15 +08:00

135 lines
7.0 KiB
Markdown
Raw Permalink 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/token/refresh`, `POST /v1/auth/logout` |
| sms | `/v1/sms` | `POST /v1/sms/codes`(`X-Language` 必填;封控见 `docs/sms.md`) |
| users | `/v1/users` | `GET/PATCH/DELETE /v1/users/me` |
| friends | `/v1/friends` | apply, requests, accept, profile, remark, pin, block, report;`GET /v1/friends/lookup?q=` |
| 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 |
| favorites | `/v1/favorites` | 收藏聊天消息 |
| moments | `/v1/moments` | 朋友圈 Feed、相册、赞评、设置 |
| square | `/v1/square` | 广场帖子、赞评、通知 |
| music | `/v1/music` | 公开 playlist / categories / settings(无需登录) |
| 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, `POST /v1/sms/codes`, token refresh, healthz, readyz, docs, `GET /v1/meta/enums`).
### OTP send rate limits (`POST /v1/sms/codes`)
Documented in **`docs/sms.md`**. Implementation:
1. **`AuthRateLimit`** on `RealIP` for `/v1/sms/codes`, `/v1/auth/login`, `/v1/auth/token/refresh` — token bucket `rate_limit.auth_per_second` (default 5).
2. **`sms.logic.Send`** — per `{channel}:{purpose}:{target}` **60s** cooldown via `CodeStore.SetCooldown` (`sms:cd:*` in Redis); OTP TTL **300s** (`sms:code:*`).
Both return **`errs.ErrRateLimited` (`42902`)**. UI countdown should follow **60s** target cooldown, not `expires_in`.
- OpenAPI `tags` must match these groups. Docs jump tags `enums` / `errors` sit above `system` and open `/docs/enums`、`/docs/errors`.
## Admin HTTP (separate from `/v1`)
Registered in `router_admin.go`. Bearer admin token (`AdminAuth`). Prefixes:
| Prefix | Use |
|--------|-----|
| `/api/admin` | Shared user lookup / ban ops (popadmin host routing) |
| `/api/admin/spark`, `/api/admin/pop` | IM stats, groups, complaints, rankings, **music CRUD** + uploads |
Music admin: `GET/POST/PATCH/DELETE …/music`, categories tree, `PUT …/music/:id/categories`, cover/background/audio upload. Client-facing music stays on `/v1/music`.
When adding admin routes: handler under `handler/admin`, register in `router_admin.go`, document in OpenAPI admin section if applicable.
## 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.