125 lines
6.9 KiB
Markdown
125 lines
6.9 KiB
Markdown
---
|
||
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`
|