6.9 KiB
| name | description |
|---|---|
| spark-im-v3 | Implements Spark IM modular monolith per architecture V3 in spark-server. Use when writing or reviewing Go, migrations/SQL, OpenAPI, TCP/gnet, Gin HTTP, admin `/api/admin`, REST `/v1`, MVC handlers, validator, RealIP, messages, deliver queue, inbox ZSET, music/square modules, Makefile lint/test/deploy, or t_* MySQL tables. Triggers: IM, MsgId, TaskPool, deliver:queue, migrations, t_users, golangci-lint, make test, SYNC_INBOX, gnet, RealIP, i18n, entity, model, logic, enum, oneof, SEND_ACK. |
Spark IM V3
Read invariants.md before changing send/sync/transport. Full design: docs/architecture-v3.md. Lint: .golangci.yaml (use GOLANGCI_LINT := golangci-lint in Makefile — not Make's builtin LINT).
MySQL / migrations: mysql.md — t_ table prefix, replay-all migrate, run migrate after SQL edits.
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.
Enums: read enums.md — typed entity enums, OpenAPI $ref, oneof bound to the same numbers.
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 (TableName() → t_*, 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 Snowflake business ids (users.id, messages.id, conversations.id).
| 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 | t_groups.id (群号); row id on t_user_devices, t_friend_relations, … (see entity docs) |
Table-local row id. Business uniqueness stays UNIQUE (email, P2P pair, members, cursors). Do not AUTO_INCREMENT Snowflake columns. |
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 (message/entity.Type 1–9). messages.content stores serialized JSON bytes (BLOB), never a raw string. Closed type/status values: enums.md.
TCP body is JSON envelope {code,msg,request_id,data}. AUTH request is {"token","device_id","app_version","language"} (not protobuf).
Public TLS is Nginx :443 SNI: HTTP cert on loopback :8443 (sparkapi + sparkr/RustFS), IM cert on loopback :19001 then PROXY to gnet :9090 (host :9000 is rustfs). 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. - Closed type/status: entity typed enum +
Valid()+ OpenAPI named$ref+ validatoroneofon the same values (enums.md). - 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). Logout isPOST /v1/auth/logout(current device). Account cancellation isDELETE /v1/users/me(archive touser_delete, purge friends/moments/related). - SQL/GORM: mysql.md (COMMENT,
t_*, migrate).
Verify before done
Follow .cursor/rules/verify-before-done.mdc: from repo root, make test (depends on check-dev, then lint) must exit 0; logic packages need ≥65% coverage (LOGIC_COVER_MIN). No Go toolchain → make test errors; install Go 1.26+ and golangci-lint (make setup). make deploy depends on make test.
Additional resources
- invariants.md
- mysql.md
- send-path.md
- http.md
- i18n.md
- enums.md
docs/architecture-v3.md