RhythMC-Reborn MySQL 表结构设计
本文档给出一版适合当前 RhythMC-Reborn 网络架构、上传流程和对战玩法的 MySQL 8 表结构方案。
设计目标:
- 支持服务器登录、玩家登录、成绩上传、对战结算、收藏品同步
- 兼顾事务一致性、排行榜查询性能与后续扩展能力
- 区分实时态与持久态,数据库只保存需要长期查询和审计的数据
设计原则
MySQL 8作为主业务库,负责账户、成绩、会话、收藏品和对战数据WebSocket实时同步状态不直接依赖数据库,建议放在内存或 Redis- 玩家单曲最佳成绩与原始上传记录分离,兼顾查询性能与审计能力
- 尽量使用明确外键、唯一约束和组合索引,保证数据一致性
- 文档中的
BIGINT主键默认使用雪花 ID、数据库号段或应用层生成 ID
核心实体关系
表结构总览
| 表名 | 用途 |
|---|---|
servers | 远程注册服信息 |
server_sessions | 服务端登录会话与在线记录 |
players | 玩家主档案 |
player_sessions | 玩家登录会话 |
player_permissions | 玩家权限明细 |
levels | 曲目/谱面元数据 |
gameplay_records | 每次上传的原始对局记录 |
player_song_records | 玩家在每张谱面的聚合最佳成绩 |
collections | 收藏品定义 |
player_collection_unlocks | 玩家已解锁收藏品 |
duel_matches | 对战房间与结算 |
duel_match_players | 对战参与者明细 |
1. 服务器相关
servers
记录后端已注册的 Minecraft 游戏服务器。
CREATE TABLE servers (
id BIGINT PRIMARY KEY,
server_name VARCHAR(64) NOT NULL,
country_code CHAR(2) NOT NULL,
public_ip VARCHAR(64) NOT NULL,
status TINYINT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_servers_name (server_name),
KEY idx_servers_country (country_code)
);
字段建议:
status: 1=active, 0=disabled- 若后续要支持服务器登录 JWT 轮换,可单独增加
server_credentials表
server_sessions
对应后端内部 server_sessions.session_uuid,用于记录服务器在线周期与审计信息。
CREATE TABLE server_sessions (
id BIGINT PRIMARY KEY,
server_id BIGINT NOT NULL,
session_uuid CHAR(36) NOT NULL,
plugin_version INT NOT NULL,
plugin_sha CHAR(64) NOT NULL,
login_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
disconnected_at DATETIME(3) NULL,
ws_connected TINYINT NOT NULL DEFAULT 0,
last_seen_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_server_sessions_uuid (session_uuid),
KEY idx_server_sessions_server_time (server_id, login_at DESC),
CONSTRAINT fk_server_sessions_server
FOREIGN KEY (server_id) REFERENCES servers(id)
);
用途:
- 对应后端 Redis / MySQL 中的服务器在线周期标识,不再作为公开插件协议字段
- 用于排查某次上传来自哪台服、哪次在线周期
2. 玩家与会话
players
玩家主档案表,保存平台内稳定主键和展示字段。
CREATE TABLE players (
id BIGINT PRIMARY KEY,
username VARCHAR(32) NOT NULL,
display_name VARCHAR(64) NULL,
player_prefix VARCHAR(128) NULL,
player_suffix VARCHAR(128) NULL,
online_uuid CHAR(36) NULL,
profile_version INT NOT NULL DEFAULT 1,
aptitude INT NOT NULL DEFAULT 0,
watt_hour BIGINT NOT NULL DEFAULT 0,
net_status TINYINT NOT NULL DEFAULT 0,
last_login_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_players_username (username),
KEY idx_players_online_uuid (last_online_uuid),
KEY idx_players_apt (aptitude DESC),
KEY idx_players_watt_hour (watt_hour DESC)
);
说明:
players.id才是平台内部唯一玩家标识,不要把minecraft_uuid当成主身份键- 正版续关UUID认证。
aptitude与watt_hour直接来自当前玩家档案模型,见docs/player-profile-json.mdprofile_version记录该玩家最近一次成功登录时客户端上报的玩家数据版本,用于在/player/login阶段做数据兼容性校验net_status0=TOKEN ONLY, 1= ONLINE, 2= OFFLINE_CONTINUE续关watt_hour当前由插件本地计算后上传覆盖;后端负责持久化、鉴权与排行榜查询
排行榜约定:
/server/baltop直接读取players.watt_hour- 默认排序为
watt_hour DESC, id ASC idx_players_watt_hour继续作为财富榜主索引
余额同步约定:
- 插件本地每次修改
PlayerData.wattHour后,通过POST /player/economy上传完整最新余额 - 后端不依赖增量日志来计算当前余额
player_sessions
保存玩家每次登录会话,用于风控、审计和上传关联。
CREATE TABLE player_sessions (
id BIGINT PRIMARY KEY,
player_id BIGINT NOT NULL,
session_uuid CHAR(36) NOT NULL,
server_session_id BIGINT NULL,
auth_provider VARCHAR(32) NOT NULL,
auth_status VARCHAR(32) NOT NULL,
login_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
logout_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_player_sessions_uuid (session_uuid),
KEY idx_player_sessions_player_time (player_id, login_at DESC),
KEY idx_player_sessions_server (server_session_id),
CONSTRAINT fk_player_sessions_player
FOREIGN KEY (player_id) REFERENCES players(id),
CONSTRAINT fk_player_sessions_server_session
FOREIGN KEY (server_session_id) REFERENCES server_sessions(id)
);
建议枚举:
auth_provider:MICROSOFT,OFFLINE,TOKENauth_status:CONNECTED_ONLINE,CONNECTED_OFFLINE,BANNED,REJECTED- 玩家登录时应先解析外部身份,再映射到
player_identities -> players.id;不要直接用minecraft_uuid查players
player_permissions
当前项目里玩家权限是独立集合,适合单独建表,而不是塞进 players 的 JSON 字段。
CREATE TABLE player_permissions (
id BIGINT PRIMARY KEY,
player_id BIGINT NOT NULL,
permission_key VARCHAR(128) NOT NULL,
granted_by VARCHAR(64) NULL,
granted_reason VARCHAR(255) NULL,
granted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
expires_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_player_permissions_unique (player_id, permission_key),
KEY idx_player_permissions_player (player_id, granted_at DESC),
KEY idx_player_permissions_key (permission_key),
KEY idx_player_permissions_expires (expires_at),
CONSTRAINT fk_player_permissions_player
FOREIGN KEY (player_id) REFERENCES players(id)
);
设计理由:
- 当前
PlayerData里permissions是集合,天然是一对多关系 - 单独建表后,查询某玩家权限、批量发放权限、做权限过期都更直接
- 若后面加入称号、活动资格、测试资格,也能复用这张表
建议约定:
- 永久权限:
expires_at IS NULL - 临时权限:到期后由定时任务清理或查询时过滤
permission_key直接存类似rhythmc:vip.basic的完整 key
3. 曲目与谱面
levels
保存谱面元数据,便于成绩与匹配引用统一的 level_id。
CREATE TABLE levels (
id BIGINT PRIMARY KEY,
level_uid VARCHAR(64) NOT NULL,
title VARCHAR(128) NOT NULL,
artist VARCHAR(128) NULL,
difficulty_name VARCHAR(32) NOT NULL,
difficulty_constant DECIMAL(5,2) NULL,
version INT NOT NULL DEFAULT 1,
is_active TINYINT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_levels_uid (level_uid),
KEY idx_levels_title (title),
KEY idx_levels_diff (difficulty_name, difficulty_constant)
);
如果你已经有资源清单系统,也可以把谱面元数据拆成:
songssong_difficulties
但在当前阶段,先用一张 levels 表更简单。
4. 原始上传记录
gameplay_records
这是最关键的一张表,对应 /player/upload 的每次上传。它保存完整事实数据,不负责最佳成绩聚合。
CREATE TABLE gameplay_records (
id BIGINT PRIMARY KEY,
player_id BIGINT NOT NULL,
player_session_id BIGINT NOT NULL,
server_id BIGINT NOT NULL,
server_session_id BIGINT NOT NULL,
level_id BIGINT NOT NULL,
record_type VARCHAR(32) NOT NULL,
acc INT NOT NULL,
max_combo INT NOT NULL,
is_rival TINYINT NOT NULL DEFAULT 0,
is_win TINYINT NOT NULL DEFAULT 0,
rival_player_id BIGINT NULL,
rival_acc INT NULL,
tap_perfect INT NOT NULL DEFAULT 0,
tap_great INT NOT NULL DEFAULT 0,
tap_miss INT NOT NULL DEFAULT 0,
hold_perfect INT NOT NULL DEFAULT 0,
hold_great INT NOT NULL DEFAULT 0,
hold_miss INT NOT NULL DEFAULT 0,
dodge_perfect INT NOT NULL DEFAULT 0,
dodge_great INT NOT NULL DEFAULT 0,
dodge_miss INT NOT NULL DEFAULT 0,
look_perfect INT NOT NULL DEFAULT 0,
look_great INT NOT NULL DEFAULT 0,
look_miss INT NOT NULL DEFAULT 0,
pass_flag TINYINT NOT NULL DEFAULT 0,
full_combo_flag TINYINT NOT NULL DEFAULT 0,
all_perfect_flag TINYINT NOT NULL DEFAULT 0,
is_new_record TINYINT NOT NULL DEFAULT 0,
player_arena VARCHAR(64) NULL,
played_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_gameplay_player_level_time (player_id, level_id, played_at DESC),
KEY idx_gameplay_level_acc (level_id, acc DESC, played_at ASC),
KEY idx_gameplay_server_time (server_id, played_at DESC),
KEY idx_gameplay_rival (rival_player_id, played_at DESC),
CONSTRAINT fk_gameplay_player
FOREIGN KEY (player_id) REFERENCES players(id),
CONSTRAINT fk_gameplay_player_session
FOREIGN KEY (player_session_id) REFERENCES player_sessions(id),
CONSTRAINT fk_gameplay_server
FOREIGN KEY (server_id) REFERENCES servers(id),
CONSTRAINT fk_gameplay_server_session
FOREIGN KEY (server_session_id) REFERENCES server_sessions(id),
CONSTRAINT fk_gameplay_level
FOREIGN KEY (level_id) REFERENCES levels(id)
);
设计理由:
- 原始上传是审计事实,不能只保留最佳成绩
- 后续排行榜纠错、封禁回滚、赛季重算都依赖这张表
idx_gameplay_level_acc可支撑单曲排行榜和榜单分页
5. 玩家单曲最佳成绩
player_song_records
聚合表,对应当前代码中的 PlayerSongRecord 结构,用于快速读取玩家最佳成绩和个人面板。
CREATE TABLE player_song_records (
id BIGINT PRIMARY KEY,
player_id BIGINT NOT NULL,
level_id BIGINT NOT NULL,
best_record_id BIGINT NOT NULL,
acc INT NOT NULL,
max_combo INT NOT NULL,
pass_flag TINYINT NOT NULL DEFAULT 0,
full_combo_flag TINYINT NOT NULL DEFAULT 0,
all_perfect_flag TINYINT NOT NULL DEFAULT 0,
play_count INT NOT NULL DEFAULT 0,
last_played_at DATETIME(3) NOT NULL,
best_played_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_player_song_records_player_level (player_id, level_id),
KEY idx_player_song_records_level_acc (level_id, acc DESC, best_played_at ASC),
KEY idx_player_song_records_player_last_played (player_id, last_played_at DESC),
CONSTRAINT fk_player_song_records_player
FOREIGN KEY (player_id) REFERENCES players(id),
CONSTRAINT fk_player_song_records_level
FOREIGN KEY (level_id) REFERENCES levels(id),
CONSTRAINT fk_player_song_records_best_record
FOREIGN KEY (best_record_id) REFERENCES gameplay_records(id)
);
更新策略:
- 每次插入
gameplay_records后,事务内UPSERT到player_song_records play_count每次加一last_played_at总是更新为最新时间- 若当前成绩优于旧成绩,则更新
best_record_id、acc、max_combo、best_played_at
这样可以对应当前 Java 模型中的:
accmaxCombopassfullComboallPerfectplayCountlastPlayedbestPlayed
6. 收藏品系统
collections
保存 /server/collections 返回的服务端定义内容。
CREATE TABLE collections (
id BIGINT PRIMARY KEY,
collection_key VARCHAR(128) NOT NULL,
collection_type VARCHAR(32) NOT NULL,
unlock_method VARCHAR(32) NOT NULL,
title_text VARCHAR(128) NULL,
position_flag TINYINT NULL,
payload_json JSON NULL,
is_active TINYINT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_collections_key (collection_key),
KEY idx_collections_type (collection_type)
);
说明:
- 可把常用字段提到列上,长尾字段放
payload_json - 这样既保留结构化查询能力,也保留后续扩展空间
player_collection_unlocks
记录玩家已解锁收藏品。
CREATE TABLE player_collection_unlocks (
id BIGINT PRIMARY KEY,
player_id BIGINT NOT NULL,
collection_id BIGINT NOT NULL,
unlock_source VARCHAR(32) NOT NULL,
unlocked_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
expires_at DATETIME(3) NULL,
UNIQUE KEY uk_player_collection_unique (player_id, collection_id),
KEY idx_player_collection_player (player_id, unlocked_at DESC),
CONSTRAINT fk_player_collection_player
FOREIGN KEY (player_id) REFERENCES players(id),
CONSTRAINT fk_player_collection_collection
FOREIGN KEY (collection_id) REFERENCES collections(id)
);
7. 对战系统
duel_matches
保存一次匹配或决斗的整体信息。
CREATE TABLE duel_matches (
id BIGINT PRIMARY KEY,
match_type VARCHAR(32) NOT NULL,
level_id BIGINT NOT NULL,
status VARCHAR(32) NOT NULL,
started_at DATETIME(3) NULL,
ended_at DATETIME(3) NULL,
winner_player_id BIGINT NULL,
server_id BIGINT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_duel_matches_status_time (status, created_at DESC),
KEY idx_duel_matches_level (level_id, created_at DESC),
CONSTRAINT fk_duel_matches_level
FOREIGN KEY (level_id) REFERENCES levels(id),
CONSTRAINT fk_duel_matches_server
FOREIGN KEY (server_id) REFERENCES servers(id)
);
duel_match_players
保存一次对战里每位玩家的参赛与结算结果。
CREATE TABLE duel_match_players (
id BIGINT PRIMARY KEY,
match_id BIGINT NOT NULL,
player_id BIGINT NOT NULL,
gameplay_record_id BIGINT NULL,
seat_no TINYINT NOT NULL,
result VARCHAR(16) NULL,
final_acc INT NULL,
final_combo INT NULL,
disconnected TINYINT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_duel_match_player (match_id, player_id),
KEY idx_duel_match_players_player (player_id, created_at DESC),
CONSTRAINT fk_duel_match_players_match
FOREIGN KEY (match_id) REFERENCES duel_matches(id),
CONSTRAINT fk_duel_match_players_player
FOREIGN KEY (player_id) REFERENCES players(id),
CONSTRAINT fk_duel_match_players_record
FOREIGN KEY (gameplay_record_id) REFERENCES gameplay_records(id)
);
这样可以支持:
- 匹配模式
- 直接决斗
- 中途断开判负
- 赛后查询历史对战
推荐查询场景
1. 查询玩家单曲最佳成绩
SELECT *
FROM player_song_records
WHERE player_id = ? AND level_id = ?;
2. 查询某张谱面的排行榜
SELECT psr.player_id, p.username, psr.acc, psr.max_combo, psr.full_combo_flag, psr.all_perfect_flag
FROM player_song_records psr
JOIN players p ON p.id = psr.player_id
WHERE psr.level_id = ?
ORDER BY psr.acc DESC, psr.best_played_at ASC
LIMIT 100;
3. 查询某玩家最近游玩记录
SELECT gr.*
FROM gameplay_records gr
WHERE gr.player_id = ?
ORDER BY gr.played_at DESC
LIMIT 50;
4. 查询某次服务器会话期间的所有上传
SELECT gr.*
FROM gameplay_records gr
WHERE gr.server_session_id = ?
ORDER BY gr.played_at DESC;
分层建议
推荐把数据分成三层:
事实表:gameplay_records,duel_matches,duel_match_players聚合表:player_song_records主数据表:players,player_identities,levels,collections,servers
这样做的优点:
- 原始数据不丢,便于纠错与重算
- 面向展示和排行榜的读取很快
- 新增赛季、活动榜、周榜时不需要推翻底层结构
与当前项目字段的映射
| 当前概念 | 推荐表 |
|---|---|
serverID / serverName | servers |
serverSessionID | server_sessions.session_uuid |
playerUID | players.id |
playerSessionID | player_sessions.session_uuid |
GameplayRecord | gameplay_records |
PlayerSongRecord | player_song_records |
permissions | player_permissions.permission_key |
aptitude | players.aptitude |
wattHour | players.watt_hour |
| 收藏品 key | collections.collection_key |
不建议直接放进 MySQL 的内容
以下数据更适合 Redis、内存或对象存储,而不是直接进主业务表:
- WebSocket 实时对手同步帧
- 心跳包与临时在线状态
- 匹配队列中的短期状态
- 大型回放文件、原始操作流、图表资源文件
如果以后要存:
- 回放二进制文件建议放对象存储
- 回放索引和元数据可以放 MySQL
- 高频事件日志可以单独放 MongoDB 或 ClickHouse
最小可行落地方案
如果你准备先做第一版后端,建议先只落这 8 张核心表:
playersplayer_sessionsplayer_permissionslevelsserversserver_sessionsgameplay_records
然后补一张:
player_song_records
这样就已经能支撑:
- 服务器登录
- 玩家登录
- 成绩上传
- 个人最佳成绩
- 排行榜
- 基础审计
收藏品和对战表可以作为第二阶段加入。
2026-03-16 更新
本文档较早章节仍保留了
server_sessions/player_sessions作为长期关系型会话主模型的设计讨论;当前实现已经切换为 Redis 持有活跃 session,MySQL 仅保留业务主数据与历史引用。当前以代码与后端
schema.sql为准:
- 活跃
server session/player session在 Redisgameplay_records.player_session_id/server_session_id为字符串 session id,而不是外键 bigintlevels表现在是轻量 chart catalog,只保存song_id、title、difficulty_slot、difficulty_constant、version、added_at等基础元数据- icon、composer、tags、unlock payload、resource-pack hash 等 manifest/谱面文件字段不再重复入库
建议结合 2026-03-16 关卡目录与 Levels Schema 更新 一起阅读。 额外修正:当前最小 chart catalog 已进一步收敛为
id、name、difficulty_constant、version、created_at,客户端按levelId白名单决定是否反序列化本地谱面文件。