feat: update Primary Key

This commit is contained in:
zhr 2026-09-20 11:38:27 +08:00
parent bebf76e582
commit f79211ddac
13 changed files with 116 additions and 28 deletions

View File

@ -58,13 +58,12 @@ Do **not** move `infrastructure` into `pkg` (or a public repo-root `pkg/`).
## IDs
Not every PK is Snowflake. Do not AUTO_INCREMENT natural keys.
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 | `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. |
| AUTO_INCREMENT | `groups.id` (群号); row id on `user_devices`, `friend_relations`, `user_login_logs`, `user_emails`, `p2p_conversations`, `conversation_members`, `group_members`, `device_sync_cursors`, `message_deletes`, `moment_audience`, `moment_likes`, `moment_settings`, `moment_hides` | 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.

View File

@ -128,6 +128,7 @@ func ignorableDDL(err error) bool {
1051, // unknown table
1060, // duplicate column
1061, // duplicate key name
1091, // can't drop; check that column/key exists
1146: // table doesn't exist
return true
default:

View File

@ -441,20 +441,22 @@ CREATE TABLE conversations (
-- 两人永远同一条会话,学 BFF 小 uid : 大 uid
CREATE TABLE p2p_conversations (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_small BIGINT NOT NULL,
user_big BIGINT NOT NULL,
conversation_id BIGINT NOT NULL,
PRIMARY KEY (user_small, user_big),
UNIQUE KEY uk_p2p_users (user_small, user_big),
UNIQUE KEY uk_conversation (conversation_id)
);
CREATE TABLE conversation_members (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
conversation_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
last_read_msg_id BIGINT NOT NULL DEFAULT 0,
joined_at DATETIME(3) NOT NULL,
muted TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (conversation_id, user_id),
UNIQUE KEY uk_conversation_members (conversation_id, user_id),
KEY idx_user_id (user_id)
);
```
@ -481,12 +483,13 @@ CREATE TABLE mailbox_events (
);
CREATE TABLE device_sync_cursors (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
device_id BIGINT NOT NULL,
mailbox_seq BIGINT NOT NULL DEFAULT 0,
dialog_score BIGINT NOT NULL DEFAULT 0, -- Redis ZSET score 游标
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (user_id, device_id)
UNIQUE KEY uk_user_device (user_id, device_id)
);
```

View File

@ -10,8 +10,9 @@ import (
// ConversationMember maps table conversation_members.
type ConversationMember struct {
ConversationID int64 `gorm:"column:conversation_id;primaryKey;comment:会话 id"`
UserID int64 `gorm:"column:user_id;primaryKey;comment:成员 user_id"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (conversation_id, user_id)"`
ConversationID int64 `gorm:"column:conversation_id;comment:会话 id"`
UserID int64 `gorm:"column:user_id;comment:成员 user_id"`
LastReadMsgID int64 `gorm:"column:last_read_msg_id;comment:该成员已读到的最大消息 id,未读真相;Redis 丢了可从此重建"`
JoinedAt time.Time `gorm:"column:joined_at;comment:加入会话时间"`
Muted int `gorm:"column:muted;comment:0=未静音 1=免打扰"`

View File

@ -2,8 +2,9 @@ package model
// P2PConversation maps table p2p_conversations.
type P2PConversation struct {
UserSmall int64 `gorm:"column:user_small;primaryKey;comment:两人中较小的 user_id,保证同一对用户只有一行"`
UserBig int64 `gorm:"column:user_big;primaryKey;comment:两人中较大的 user_id"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (user_small, user_big)"`
UserSmall int64 `gorm:"column:user_small;comment:两人中较小的 user_id,保证同一对用户只有一行"`
UserBig int64 `gorm:"column:user_big;comment:两人中较大的 user_id"`
ConversationID int64 `gorm:"column:conversation_id;comment:对应 conversations.id"`
}

View File

@ -10,8 +10,9 @@ import (
// GroupMember maps table group_members.
type GroupMember struct {
GroupID int64 `gorm:"column:group_id;primaryKey;comment:群号,groups.id"`
UserID int64 `gorm:"column:user_id;primaryKey;comment:成员 user_id"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (group_id, user_id)"`
GroupID int64 `gorm:"column:group_id;comment:群号,groups.id"`
UserID int64 `gorm:"column:user_id;comment:成员 user_id"`
Role int `gorm:"column:role;comment:1=群主 2=管理员 3=普通成员"`
Nickname string `gorm:"column:nickname;comment:群内昵称,空则用用户昵称"`
Remark string `gorm:"column:remark;comment:成员对该群的备注,仅自己可见"`

View File

@ -2,10 +2,11 @@ package model
import "time"
// MessageDelete maps table message_deletes. PK is (user_id, msg_id).
// MessageDelete maps table message_deletes. Business unique key is (user_id, msg_id).
type MessageDelete struct {
UserID int64 `gorm:"column:user_id;primaryKey;comment:删除者 user_id,仅该用户历史不可见"`
MsgID int64 `gorm:"column:msg_id;primaryKey;comment:被删除的 Snowflake MsgId"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (user_id, msg_id)"`
UserID int64 `gorm:"column:user_id;comment:删除者 user_id,仅该用户历史不可见"`
MsgID int64 `gorm:"column:msg_id;comment:被删除的 Snowflake MsgId"`
ConversationID int64 `gorm:"column:conversation_id;comment:所属会话,便于按会话过滤"`
CreatedAt time.Time `gorm:"column:created_at;comment:删除时间"`
}

View File

@ -56,16 +56,18 @@ func postFrom(p *entity.Post) *Post {
// Audience maps table moment_audience.
type Audience struct {
PostID int64 `gorm:"column:post_id;primaryKey;comment:帖 id"`
PeerID int64 `gorm:"column:peer_id;primaryKey;comment:白名单能看 / 黑名单不能看 的好友 uid"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (post_id, peer_id)"`
PostID int64 `gorm:"column:post_id;comment:帖 id"`
PeerID int64 `gorm:"column:peer_id;comment:白名单能看 / 黑名单不能看 的好友 uid"`
}
func (Audience) TableName() string { return "moment_audience" }
// Like maps table moment_likes.
type Like struct {
PostID int64 `gorm:"column:post_id;primaryKey;comment:帖 id"`
UserID int64 `gorm:"column:user_id;primaryKey;comment:点赞人"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (post_id, user_id)"`
PostID int64 `gorm:"column:post_id;comment:帖 id"`
UserID int64 `gorm:"column:user_id;comment:点赞人"`
CreatedAt time.Time `gorm:"column:created_at;comment:点赞时间"`
}
@ -146,7 +148,8 @@ func (r *Notice) Entity() *entity.Notice {
// Setting maps table moment_settings.
type Setting struct {
UserID int64 `gorm:"column:user_id;primaryKey;comment:用户 users.id"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 user_id"`
UserID int64 `gorm:"column:user_id;comment:用户 users.id"`
CoverKey string `gorm:"column:cover_key;comment:封面 object_key,空表示未设"`
VisibleDays int16 `gorm:"column:visible_days;comment:允许朋友查看范围:0全部 3/30/180 天"`
LastReadPostID int64 `gorm:"column:last_read_post_id;comment:发现页红点水位,只增不减"`
@ -180,8 +183,9 @@ func settingFrom(s *entity.Settings) *Setting {
// Hide maps table moment_hides.
type Hide struct {
UserID int64 `gorm:"column:user_id;primaryKey;comment:设置方"`
PeerID int64 `gorm:"column:peer_id;primaryKey;comment:对方"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (user_id, peer_id)"`
UserID int64 `gorm:"column:user_id;comment:设置方"`
PeerID int64 `gorm:"column:peer_id;comment:对方"`
HideHim bool `gorm:"column:hide_him;comment:1=不看他(Feed 去掉对方)"`
HideMe bool `gorm:"column:hide_me;comment:1=不让他看"`
}

View File

@ -79,7 +79,10 @@ func (m *Repo) AddLike(ctx context.Context, like *entity.Like) (bool, error) {
}
created := false
err = db.Transaction(func(tx *gorm.DB) error {
res := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row)
res := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "post_id"}, {Name: "user_id"}},
DoNothing: true,
}).Create(&row)
if res.Error != nil {
return mapDBErr(res.Error)
}
@ -197,7 +200,12 @@ func (m *Repo) UpsertSettings(ctx context.Context, s *entity.Settings) error {
if row.UpdatedAt.IsZero() {
row.UpdatedAt = time.Now()
}
return mapDBErr(db.Save(row).Error)
return mapDBErr(db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}},
DoUpdates: clause.AssignmentColumns([]string{
"cover_key", "visible_days", "last_read_post_id", "updated_at",
}),
}).Create(row).Error)
}
func (m *Repo) AdvanceRead(ctx context.Context, userID, postID int64) error {

View File

@ -4,8 +4,9 @@ import "time"
// DeviceSyncCursor maps table device_sync_cursors.
type DeviceSyncCursor struct {
UserID int64 `gorm:"column:user_id;primaryKey;comment:用户 id"`
DeviceID int64 `gorm:"column:device_id;primaryKey;comment:设备 id"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 (user_id, device_id)"`
UserID int64 `gorm:"column:user_id;comment:用户 id"`
DeviceID int64 `gorm:"column:device_id;comment:设备 id"`
MailboxSeq int64 `gorm:"column:mailbox_seq;comment:该设备已同步到的 mailbox_events.seq"`
DialogScore int64 `gorm:"column:dialog_score;comment:会话列表 Redis ZSET 游标"`
UpdatedAt time.Time `gorm:"column:updated_at;comment:游标更新时间"`

View File

@ -7,6 +7,7 @@ import (
mysqldriver "github.com/go-sql-driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"spark-server/internal/module/sync/entity"
"spark-server/internal/pkg/errs"
@ -119,5 +120,8 @@ func (s *Store) SaveCursor(ctx context.Context, userID int64, deviceID uint64, m
UserID: userID, DeviceID: int64(deviceID),
MailboxSeq: mailboxSeq, DialogScore: dialogScore, UpdatedAt: time.Now(),
}
return mapDBErr(db.Save(&row).Error)
return mapDBErr(db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "device_id"}},
DoUpdates: clause.AssignmentColumns([]string{"mailbox_seq", "dialog_score", "updated_at"}),
}).Create(&row).Error)
}

View File

@ -11,7 +11,8 @@ import (
// UserEmail maps table user_emails: one normalized email to one user.
type UserEmail struct {
Email string `gorm:"column:email;primaryKey;comment:规范化后的小写邮箱,登录账号"`
ID int64 `gorm:"column:id;primaryKey;autoIncrement;comment:行 id,表内自增,仅内部用;业务唯一键是 email"`
Email string `gorm:"column:email;comment:规范化后的小写邮箱,登录账号"`
UserID int64 `gorm:"column:user_id;comment:绑定的 users.id,一对一"`
CreatedAt time.Time `gorm:"column:created_at;comment:绑定时间"`
}

View File

@ -0,0 +1,63 @@
-- Junction / mapping tables get a table-local AUTO_INCREMENT row id.
-- Business uniqueness stays UNIQUE (same columns as the old composite PK).
-- Snowflake PKs (users / messages / conversations / moments posts) are unchanged.
ALTER TABLE user_emails
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 email' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_email (email);
ALTER TABLE p2p_conversations
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (user_small, user_big)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_p2p_users (user_small, user_big);
ALTER TABLE conversation_members
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (conversation_id, user_id)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_conversation_members (conversation_id, user_id);
ALTER TABLE device_sync_cursors
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (user_id, device_id)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_user_device (user_id, device_id);
ALTER TABLE group_members
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (group_id, user_id)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_group_members (group_id, user_id);
ALTER TABLE message_deletes
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (user_id, msg_id)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_user_msg (user_id, msg_id);
ALTER TABLE moment_audience
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (post_id, peer_id)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_post_peer (post_id, peer_id);
ALTER TABLE moment_likes
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (post_id, user_id)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_post_user (post_id, user_id);
ALTER TABLE moment_settings
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 user_id' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_user_id (user_id);
ALTER TABLE moment_hides
DROP PRIMARY KEY,
ADD COLUMN id BIGINT NOT NULL AUTO_INCREMENT COMMENT '行 id,表内自增,仅内部用;业务唯一键是 (user_id, peer_id)' FIRST,
ADD PRIMARY KEY (id),
ADD UNIQUE KEY uk_user_peer (user_id, peer_id);