跳到主要内容

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认证。
  • aptitudewatt_hour 直接来自当前玩家档案模型,见 docs/player-profile-json.md
  • profile_version 记录该玩家最近一次成功登录时客户端上报的玩家数据版本,用于在 /player/login 阶段做数据兼容性校验
  • net_status 0=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, TOKEN
  • auth_status: CONNECTED_ONLINE, CONNECTED_OFFLINE, BANNED, REJECTED
  • 玩家登录时应先解析外部身份,再映射到 player_identities -> players.id;不要直接用 minecraft_uuidplayers

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)
);

设计理由:

  • 当前 PlayerDatapermissions 是集合,天然是一对多关系
  • 单独建表后,查询某玩家权限、批量发放权限、做权限过期都更直接
  • 若后面加入称号、活动资格、测试资格,也能复用这张表

建议约定:

  • 永久权限: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)
);

如果你已经有资源清单系统,也可以把谱面元数据拆成:

  • songs
  • song_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 后,事务内 UPSERTplayer_song_records
  • play_count 每次加一
  • last_played_at 总是更新为最新时间
  • 若当前成绩优于旧成绩,则更新 best_record_idaccmax_combobest_played_at

这样可以对应当前 Java 模型中的:

  • acc
  • maxCombo
  • pass
  • fullCombo
  • allPerfect
  • playCount
  • lastPlayed
  • bestPlayed

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 / serverNameservers
serverSessionIDserver_sessions.session_uuid
playerUIDplayers.id
playerSessionIDplayer_sessions.session_uuid
GameplayRecordgameplay_records
PlayerSongRecordplayer_song_records
permissionsplayer_permissions.permission_key
aptitudeplayers.aptitude
wattHourplayers.watt_hour
收藏品 keycollections.collection_key

不建议直接放进 MySQL 的内容

以下数据更适合 Redis、内存或对象存储,而不是直接进主业务表:

  • WebSocket 实时对手同步帧
  • 心跳包与临时在线状态
  • 匹配队列中的短期状态
  • 大型回放文件、原始操作流、图表资源文件

如果以后要存:

  • 回放二进制文件建议放对象存储
  • 回放索引和元数据可以放 MySQL
  • 高频事件日志可以单独放 MongoDB 或 ClickHouse

最小可行落地方案

如果你准备先做第一版后端,建议先只落这 8 张核心表:

  1. players
  2. player_sessions
  3. player_permissions
  4. levels
  5. servers
  6. server_sessions
  7. gameplay_records

然后补一张:

  1. player_song_records

这样就已经能支撑:

  • 服务器登录
  • 玩家登录
  • 成绩上传
  • 个人最佳成绩
  • 排行榜
  • 基础审计

收藏品和对战表可以作为第二阶段加入。

2026-03-16 更新

本文档较早章节仍保留了 server_sessions / player_sessions 作为长期关系型会话主模型的设计讨论;当前实现已经切换为 Redis 持有活跃 session,MySQL 仅保留业务主数据与历史引用。

当前以代码与后端 schema.sql 为准:

  • 活跃 server session / player session 在 Redis
  • gameplay_records.player_session_id / server_session_id 为字符串 session id,而不是外键 bigint
  • levels 表现在是轻量 chart catalog,只保存 song_idtitledifficulty_slotdifficulty_constantversionadded_at 等基础元数据
  • icon、composer、tags、unlock payload、resource-pack hash 等 manifest/谱面文件字段不再重复入库

建议结合 2026-03-16 关卡目录与 Levels Schema 更新 一起阅读。 额外修正:当前最小 chart catalog 已进一步收敛为 idnamedifficulty_constantversioncreated_at,客户端按 levelId 白名单决定是否反序列化本地谱面文件。