新增 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>
7.0 KiB
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,/sendMessageunder 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:
AuthRateLimitonRealIPfor/v1/sms/codes,/v1/auth/login,/v1/auth/token/refresh— token bucketrate_limit.auth_per_second(default 5).sms.logic.Send— per{channel}:{purpose}:{target}60s cooldown viaCodeStore.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
tagsmust match these groups. Docs jump tagsenums/errorssit abovesystemand 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
- Input struct in
internal/module/{biz}withvalidate/label/errmsgkeys. - Use-case method on a service in that module (HTTP and TCP share it).
- Thin handler under
internal/transport/http/handler/{biz}. - Register on the matching
v1.Groupinrouter.go. - Document the path and tag in
internal/transport/http/docs/openapi.yaml. - Assert
reqctx.RealIP(ctx)wherever you rate-limit or audit. - Localize errors/notifications via
internal/pkg/i18n; never hardcodemsg. - Closed type/status: entity typed enum + OpenAPI named schema
$ref(enums.md). Do not inline a secondenum:list.