5.4 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/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,/sendMessageunder 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
tagsmust match these groups. Docs jump tagsenums/errorssit abovesystemand open/docs/enums、/docs/errors.
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.