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

121 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: spark-im-v3
description: >-
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](invariants.md) before changing send/sync/transport. Full design: `docs/architecture-v3.md`. Lint: `.golangci.yaml`.
## Layout
```text
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](http.md) — validator keys, `reqctx.RealIP`, `/v1/{resource}` groups, MVC login.
i18n: read [i18n.md](i18n.md) — request language, error `msg`, notification types.
Enums: read [enums.md](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](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](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
- [invariants.md](invariants.md)
- [send-path.md](send-path.md)
- [http.md](http.md)
- [i18n.md](i18n.md)
- [enums.md](enums.md)
- `docs/architecture-v3.md`