96 lines
5.7 KiB
Markdown
96 lines
5.7 KiB
Markdown
# Enums: typed closed sets for JSON, TCP, and SQL
|
||
|
||
Closed type/status values live in `internal/module/{biz}/entity` as named types. OpenAPI names the same set once and `$ref`s it. Do not invent a parallel string vocabulary.
|
||
|
||
`GET /v1/meta/enums` is the client bootstrap (no auth). Human pages: `/docs/enums` (jumpable sets) and `/docs/errors` (`envelope_code`). `value` is the wire number or string; `name` is a stable English token; `label` is i18n for the request language. Values are built from entity consts — do not hand-copy numbers into the handler.
|
||
|
||
Wire JSON and TCP stay **numbers** (or the existing strings below). Do not turn message types into strings. Do not change persisted numeric values.
|
||
|
||
## Template (`message/entity.Type`)
|
||
|
||
```go
|
||
type Type int16
|
||
|
||
const (
|
||
TypeText Type = 1
|
||
// ...
|
||
TypeSystem Type = 9
|
||
)
|
||
|
||
func (t Type) Valid() bool {
|
||
return t >= TypeText && t <= TypeSystem
|
||
}
|
||
```
|
||
|
||
Input DTOs that bind the field directly use the same numbers in `validate:"oneof=..."`:
|
||
|
||
```go
|
||
Gender *entity.Gender `json:"gender" validate:"omitempty,oneof=0 1 2" label:"field.gender" errmsg:"oneof:validate.user.gender_invalid"`
|
||
```
|
||
|
||
Nested JSON (e.g. `content.t`) is not a validator field; gate it with `Valid()` inside `UnmarshalContent` / `Content.Validate`.
|
||
|
||
## Layers
|
||
|
||
| Layer | Rule |
|
||
|-------|------|
|
||
| entity | `type X int` / `int16` / `string` + const + `Valid()`. Domain structs use the typed field. |
|
||
| model | GORM rows stay `int` / `int16` / `string`. Convert at `entity()` / `rowFrom`. Never store a Go typed enum in a GORM struct if the column is numeric. |
|
||
| logic JSON | Response/request DTOs use the entity type so encoding/json emits the number (or the string). |
|
||
| HTTP validator | `oneof` lists the **same** numbers or strings as the consts. i18n `errmsg` for `oneof`. |
|
||
| OpenAPI | One named schema in `components/schemas`; fields `$ref` it. Do not inline `enum:` on the field. |
|
||
| HTTP catalog | Add the set to `meta/logic.Catalog` and i18n `enum.{set}.{name}`. Served by `GET /v1/meta/enums`. |
|
||
|
||
Name clashes: the JSON viewer `relation` is `RelationKind`; the table column is `EdgeStatus`. Do not name both `Status` in the same package if two tables differ.
|
||
|
||
## Numeric catalog
|
||
|
||
Values are stable. Do not reuse a retired number.
|
||
|
||
| OpenAPI schema | Go | JSON / TCP | SQL | Values |
|
||
|----------------|----|------------|-----|--------|
|
||
| `Gender` | `user/entity.Gender` | `gender` | `users.gender` | 0未知 1男 2女 |
|
||
| `UserStatus` | `user/entity.Status` | `status` | `users.status` | 0停用 1正常 |
|
||
| `ConversationType` | `conversation/entity.Type` | (internal / conv type) | `conversations.type` | 1单聊 2群聊 |
|
||
| `MessageKind` | `message/entity.Kind` | `kind` | not a column; selects `private_message` vs `group_message` | 1 / 2, **same numbers as ConversationType** |
|
||
| `MessageType` | `message/entity.Type` | `message_type`, `content.t` | `*_message.message_type` | 1文本 2图片 3语音 4视频 5表情 6GIF 7文件 8位置 9系统 |
|
||
| `MessageStatus` | `message/entity.Status` | `status` | `*_message.status` | 1正常 2已撤回 3审核隐藏 |
|
||
| `RelationKind` | `friend/entity.RelationKind` | `relation` | not a column (viewer-centric) | 0无 1好友 2已拉黑 |
|
||
| — | `friend/entity.EdgeStatus` | not on the wire | `friend_relations.status` | 0非好友 1好友 |
|
||
| `FriendRequestStatus` | `friend/entity.RequestStatus` | apply `status` | `friend_requests.status` | 1待处理 2已同意 3已拒绝 |
|
||
| `FriendSource` | `friend/entity.Source` | `source` | request + edge `source` | 0未知 1二维码 2邮箱 3spark_id |
|
||
| `ReportReason` | `friend/entity.ReportReason` | `reason` | `friend_reports.reason` | 1垃圾 2骚扰 3不实身份 4其他 |
|
||
| `GroupStatus` | `group/entity.Status` | (internal) | `groups.status` | 1正常 2已解散 |
|
||
| `GroupRole` | `group/entity.Role` | `role` | `group_members.role` | 1群主 2管理员 3成员。PATCH 只能 2 或 3 |
|
||
| `MailboxEventType` | `sync/entity.EventType` | mailbox `event_type` | `mailbox_events.event_type` | 1漏推 2撤回 3删除 |
|
||
| `TcpCommand` | `tcp/protocol.Cmd*` | Frame.Command | — | 1–15 see protocol |
|
||
| `EnvelopeCode` | `pkg/errs` codes | envelope `code` | — | `"0"` + `40001`… |
|
||
|
||
`POST /v1/friends/requests` returns `FriendApplyData.status` as **1 or 2**, not `"pending"` / `"accepted"`.
|
||
|
||
Compare `MessageKind` with `conversation.entity.Type`, never with `message.entity.Type`.
|
||
|
||
## String catalog (keep as strings)
|
||
|
||
Do not convert these to integers.
|
||
|
||
| OpenAPI schema | Go | JSON | Values |
|
||
|----------------|----|------|--------|
|
||
| `DeviceType` | login DTO `string` + `oneof` | `device_type` / `platform` | `ios android harmony windows macos web` |
|
||
| `Language` | `i18n.Lang` | `language`, `Accept-Language`, `X-Language` | `zh-CN` `en` `zh-TW` (default `zh-CN`) |
|
||
| `UploadPurpose` | `upload/entity.Purpose*` consts + `LookupSpec` | `purpose` | `avatar group_avatar chat_image chat_voice chat_video chat_gif chat_file` |
|
||
| `SystemKind` | `friend/entity.SystemKind*` / `group/entity.SystemKind*` | `content.system.kind` | `friend.became` `group.created` `group.invite` `group.leave` `group.kick` `group.dissolve` `group.transfer` |
|
||
|
||
Health `status` (`ok` / `not_ready`) is process liveness, not a domain enum.
|
||
|
||
## Adding a closed set
|
||
|
||
1. Put `type X int` (or keep a string const set) in the owning module `entity`. Add `Valid()`.
|
||
2. Use `X` on entity + JSON DTOs. Convert at the GORM boundary.
|
||
3. Bind request fields with `oneof=` listing the same values; add i18n `errmsg`.
|
||
4. Add a named schema in `internal/transport/http/docs/openapi.yaml` and `$ref` every field.
|
||
5. Add the set to `GET /v1/meta/enums` (`meta/logic`) and i18n labels `enum.{set}.{name}` (zh-CN / en / zh-TW).
|
||
6. Update this catalog.
|
||
|
||
Do **not**: change existing numbers; stringify a numeric set; duplicate enum lists in handlers.
|