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

7.0 KiB
Raw Blame History

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.

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), 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):

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.

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). Do not inline a second enum: list.