spark-server/.cursor/skills/spark-im-v3/SKILL.md
2026-09-18 14:50:41 +08:00

6.4 KiB
Raw Blame History

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, enum, MessageType, oneof.

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. 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 (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 (群号), friend_relations.id Internal row id, or sequential public 群号. Friend edges are directed (user_id→friend_id); UNIQUE (user_id, friend_id). 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)

  1. TaskPool (not EventLoop)
  2. Permission (friend / block / group mute) — fail before INSERT
  3. Snowflake MsgId (or client MsgId within ±5m)
  4. MySQL txn: messages + conversations.last_msg_id
  5. DeliverQueue.Push (reliable list)
  6. 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, 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 i18n label/errmsg keys; put RealIP and language on context.
  • Closed type/status: entity typed enum + Valid() + OpenAPI named $ref + validator oneof on the same values (enums.md).
  • Login is email OTP (POST /v1/auth/login). uid is Snowflake users.id; public spark_id is unique and mutable. Email↔user lives in user_emails. Login upserts user_devices (client device_id).
  • SQL tables and GORM models carry COMMENT on the table and every column.
  • After adding or changing migrations/*.sql, run go run ./cmd/migrate -config configs/config.yaml in this session. Do not leave it for deploy.
  • Run make lint (funlen 100).

Additional resources