Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout. Co-authored-by: Cursor <cursoragent@cursor.com>
5.9 KiB
| name | description |
|---|---|
| spark-im-v3 | Implements Spark IM modular monolith per architecture V3. Use when writing or reviewing spark-server Go code, TCP/gnet, Gin HTTP, REST /v1 groups, MVC handlers, go-playground validator, RealIP, messages, deliver queue, inbox ZSET, Nginx 443 SNI, PROXY protocol, or SEND_ACK. Trigger terms: IM, MsgId, TaskPool, deliver:queue, SYNC_INBOX, gnet, RealIP, validator, LoginService, i18n, Accept-Language, zh-CN, uid, spark_id, infrastructure, entity, model, logic. |
Spark IM V3
Read invariants.md before changing send/sync/transport. Full design: docs/architecture-v3.md. Lint: .golangci.yaml.
Layout
cmd/server process entry
cmd/migrate apply migrations/*.sql (re-runs all files)
internal/bootstrap config, lifecycle, wire model → logic
internal/transport/http Gin engine, REST groups, middleware
internal/transport/http/handler thin MVC controllers (no business)
internal/transport/http/respond BindJSON + envelope write
internal/transport/tcp gnet EventLoop + TaskPool + Frame
internal/module/{biz} entity / model / logic (see Layers)
internal/pkg process-agnostic helpers (no DSN / pools)
internal/infrastructure process adapters (mysql, redis, id, logger, push)
HTTP: read http.md — validator keys, reqctx.RealIP, /v1/{resource} groups, MVC login.
i18n: read i18n.md — request language, error msg, notification types.
Layers
Each internal/module/{biz} is three packages. Do not put entity, GORM rows, and use-cases in one directory.
| Layer | Path | Owns |
|---|---|---|
| entity | {biz}/entity |
Domain types, message payloads. No I/O. |
| model | {biz}/model |
One Go file per MySQL table (GORM + COMMENT). Redis adapters. |
| logic | {biz}/logic |
One file per function (profile, login, send). Not per table. |
logic must not import model. Wire implementations in internal/bootstrap. Auth has no tables (entity + logic only). HTTP/TCP both call logic; Gin handlers only bind and write the envelope.
pkg vs infrastructure
Do not move infrastructure into pkg (or a public repo-root pkg/).
| Path | Owns | May import |
|---|---|---|
internal/pkg |
errs, i18n, validate, reqctx, reply, in-memory ratelimit |
stdlib + other pkg only |
internal/infrastructure |
MySQL pool, Redis client, Snowflake id, zap logger, push, metrics |
pkg + vendor drivers |
pkg has no connection lifecycle and must not know DSN, Redis AUTH, or snowflake_worker_id. infrastructure is opened/closed by bootstrap. Direction: infrastructure → pkg, never the reverse.
IDs
Not every PK is Snowflake. Do not AUTO_INCREMENT natural keys.
| Kind | Examples | Rule |
|---|---|---|
| Snowflake | users.id (uid), messages.id (MsgId), conversations.id |
Time-ordered business IDs. One id.Generator per process; snowflake_worker_id unique per process. Clock rewind fails issuance for the whole process. |
| AUTO_INCREMENT | user_devices.id, groups.id (群号) |
Internal row id, or sequential public 群号. Business chat cursor is still Snowflake conversations.id. |
| Natural key | user_emails.email, P2P (user_small, user_big), members, sync cursors |
Composite / natural PK. No extra surrogate id. |
device_id is client-generated, stored on the device, written to JWT did. The server does not allocate it. uid in JSON is users.id; do not add a second uid column.
Send path (must keep this order)
- TaskPool (not EventLoop)
- Permission (friend / block / group mute) — fail before INSERT
- Snowflake MsgId (or client MsgId within ±5m)
- MySQL txn:
messages+conversations.last_msg_id DeliverQueue.Push(reliable list)SEND_ACK
If step 5 fails, do not ACK. Client retries the same client_msg_id.
EventLoop vs pools
| Pool | Owns |
|---|---|
| gnet EventLoop | PROXY, TLS, Frame, heartbeat, PUSH_ACK, Submit |
| TaskPool | AUTH, SEND, SYNC, READ, RECALL, MySQL |
| Deliver Worker | ZSET inbox, Session.Send, mailbox on push fail, PushNotifier |
Commands
AUTH HEARTBEAT GENERATE_MSG_ID SEND_MESSAGE SEND_ACK MESSAGE PUSH_ACK READ RECALL SYNC_INBOX SYNC_MESSAGES KICK
Media: HTTP upload credential, message content is object key only. Frame max 64KB. Compress body over 512B.
Message types: text / image / voice / video / emoji / gif / file / location / system. messages.content stores serialized JSON bytes (BLOB), never a raw string.
TCP body is JSON envelope {code,msg,data}. AUTH request is {"token","device_id","app_version","language"} (not protobuf).
Public TLS is Nginx :443 SNI: HTTP cert on loopback :8443, IM cert on loopback :9001 then PROXY to gnet :9000. See deploy/nginx/.
AUTH/login rate-limit uses reqctx.RealIP. TaskPool workers recover panics. Deliver workers start with RequeueStale + SETNX deliver:done:{msgId}.
When adding a feature
- HTTP writes must enqueue deliver jobs for online devices.
- List/unread go to Redis ZSET, not MySQL row-per-member.
- New goroutine I/O does not belong in
OnTraffic. - HTTP: thin handler + module
logic; REST group by business; validate with i18nlabel/errmsgkeys; put RealIP and language on context. - Login is email OTP (
POST /v1/auth/login).uidis Snowflakeusers.id; publicspark_idis unique and mutable. Email↔user lives inuser_emails. Login upsertsuser_devices(clientdevice_id). - SQL tables and GORM models carry COMMENT on the table and every column.
- After adding or changing
migrations/*.sql, rungo run ./cmd/migrate -config configs/config.yamlin this session. Do not leave it for deploy. - Run
make lint(funlen 100).
Additional resources
- invariants.md
- send-path.md
- http.md
- i18n.md
docs/architecture-v3.md