spark-server/.cursor/skills/spark-im-v3/SKILL.md
2026-09-29 10:22:28 +08:00

125 lines
6.9 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 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](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](mysql.md) — **`t_` table prefix**, replay-all migrate, run migrate after SQL edits.
## 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 (`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)
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` (`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 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`). Logout is `POST /v1/auth/logout` (current device). Account cancellation is `DELETE /v1/users/me` (archive to `user_delete`, purge friends/moments/related).
- SQL/GORM: [mysql.md](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](invariants.md)
- [mysql.md](mysql.md)
- [send-path.md](send-path.md)
- [http.md](http.md)
- [i18n.md](i18n.md)
- [enums.md](enums.md)
- `docs/architecture-v3.md`