【文字三国志:第三篇】天命重构,数据模型设计

数据模型设计

本章将详细介绍项目的数据模型设计。我选择了 Prisma ORM 配合 PostgreSQL 作为持久化方案,所有业务实体统一在 prisma/schema.prisma 中声明,并通过 prisma generate 自动生成 TypeScript 类型------这意味着在编写业务代码时,数据库操作会获得完整的类型提示和编译时检查。

简单说:模型即代码------表结构变了,类型自动跟着变,避免了手写 SQL 时常见的字段名拼写错误。


1. 介绍之前

在开始逐个介绍模型之前,不妨先思考一个问题:一个典型的游戏服务器需要记录哪些数据?

  • 谁在玩?(用户账户)
  • 玩家长什么样?(昵称、等级、头像等公开信息)
  • 玩到哪了?(当前的游戏状态,用于断线重连)
  • 游戏里有哪些角色/势力?(由策划配置)
  • 玩家之间有哪些关系?(联盟、帮会)
  • 做了哪些值得记录的事?(成就、战斗日志)

基于这些问题,抽象出 9 个核心模型,它们之间的关系可以用下图表示:
#mermaid-svg-Yg9RlPgxgVVQDJ0k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .error-icon{fill:#552222;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .marker.cross{stroke:#333333;}#mermaid-svg-Yg9RlPgxgVVQDJ0k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Yg9RlPgxgVVQDJ0k p{margin:0;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-Yg9RlPgxgVVQDJ0k .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .node rect,#mermaid-svg-Yg9RlPgxgVVQDJ0k .node circle,#mermaid-svg-Yg9RlPgxgVVQDJ0k .node ellipse,#mermaid-svg-Yg9RlPgxgVVQDJ0k .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-Yg9RlPgxgVVQDJ0k .edgeLabel .label text{fill:#333;}#mermaid-svg-Yg9RlPgxgVVQDJ0k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} has
has
unlocks
unlocked_by
belongs_to
produces
activates
participates
User
Profile
GameState
UserAchievement
Achievement
Alliance
CombatLog
SeasonEvent
Policy
Character


2. 核心模型详解

2.1 玩家账户与档案

Model 主要字段 说明
User id, username, email, createdAt, updatedAt 玩家账户信息,关联 ProfileGameState
Profile id, userId, nickname, avatarUrl, level, experience 玩家公开资料,展示在 UI 中

把账户和档案分开

User 存储的是认证相关 的敏感信息(邮箱、密码哈希等),而 Profile 存储的是展示相关的公开信息。分开有两个好处:

  1. 查询排行榜时,不需要触碰 User 表,减少敏感数据暴露风险。
  2. 昵称、头像等频繁修改的字段不会影响认证表的主键索引。

#mermaid-svg-c6O6nyOXXSPderPo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-c6O6nyOXXSPderPo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-c6O6nyOXXSPderPo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-c6O6nyOXXSPderPo .error-icon{fill:#552222;}#mermaid-svg-c6O6nyOXXSPderPo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-c6O6nyOXXSPderPo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-c6O6nyOXXSPderPo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-c6O6nyOXXSPderPo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-c6O6nyOXXSPderPo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-c6O6nyOXXSPderPo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-c6O6nyOXXSPderPo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-c6O6nyOXXSPderPo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-c6O6nyOXXSPderPo .marker.cross{stroke:#333333;}#mermaid-svg-c6O6nyOXXSPderPo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-c6O6nyOXXSPderPo p{margin:0;}#mermaid-svg-c6O6nyOXXSPderPo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-c6O6nyOXXSPderPo .cluster-label text{fill:#333;}#mermaid-svg-c6O6nyOXXSPderPo .cluster-label span{color:#333;}#mermaid-svg-c6O6nyOXXSPderPo .cluster-label span p{background-color:transparent;}#mermaid-svg-c6O6nyOXXSPderPo .label text,#mermaid-svg-c6O6nyOXXSPderPo span{fill:#333;color:#333;}#mermaid-svg-c6O6nyOXXSPderPo .node rect,#mermaid-svg-c6O6nyOXXSPderPo .node circle,#mermaid-svg-c6O6nyOXXSPderPo .node ellipse,#mermaid-svg-c6O6nyOXXSPderPo .node polygon,#mermaid-svg-c6O6nyOXXSPderPo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-c6O6nyOXXSPderPo .rough-node .label text,#mermaid-svg-c6O6nyOXXSPderPo .node .label text,#mermaid-svg-c6O6nyOXXSPderPo .image-shape .label,#mermaid-svg-c6O6nyOXXSPderPo .icon-shape .label{text-anchor:middle;}#mermaid-svg-c6O6nyOXXSPderPo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-c6O6nyOXXSPderPo .rough-node .label,#mermaid-svg-c6O6nyOXXSPderPo .node .label,#mermaid-svg-c6O6nyOXXSPderPo .image-shape .label,#mermaid-svg-c6O6nyOXXSPderPo .icon-shape .label{text-align:center;}#mermaid-svg-c6O6nyOXXSPderPo .node.clickable{cursor:pointer;}#mermaid-svg-c6O6nyOXXSPderPo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-c6O6nyOXXSPderPo .arrowheadPath{fill:#333333;}#mermaid-svg-c6O6nyOXXSPderPo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-c6O6nyOXXSPderPo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-c6O6nyOXXSPderPo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c6O6nyOXXSPderPo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-c6O6nyOXXSPderPo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c6O6nyOXXSPderPo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-c6O6nyOXXSPderPo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-c6O6nyOXXSPderPo .cluster text{fill:#333;}#mermaid-svg-c6O6nyOXXSPderPo .cluster span{color:#333;}#mermaid-svg-c6O6nyOXXSPderPo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-c6O6nyOXXSPderPo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-c6O6nyOXXSPderPo rect.text{fill:none;stroke-width:0;}#mermaid-svg-c6O6nyOXXSPderPo .icon-shape,#mermaid-svg-c6O6nyOXXSPderPo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c6O6nyOXXSPderPo .icon-shape p,#mermaid-svg-c6O6nyOXXSPderPo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-c6O6nyOXXSPderPo .icon-shape .label rect,#mermaid-svg-c6O6nyOXXSPderPo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c6O6nyOXXSPderPo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-c6O6nyOXXSPderPo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-c6O6nyOXXSPderPo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 展示域
认证域
1:1
User

id, email, password_hash
Profile

nickname, avatar, level, exp

2.2 游戏状态与日志

Model 主要字段 说明
GameState id, userId, currentTurn, lastSavedAt 保存玩家当前的游戏状态快照(JSON 列),用于断线恢复
CombatLog id, gameStateId, payload (JSON), createdAt 每回合的战斗日志,供回放与审计使用

断线恢复的工作原理:
PostgreSQL 服务器 客户端 PostgreSQL 服务器 客户端 #mermaid-svg-m2AtBgYaCIA5z5QG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m2AtBgYaCIA5z5QG .error-icon{fill:#552222;}#mermaid-svg-m2AtBgYaCIA5z5QG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m2AtBgYaCIA5z5QG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m2AtBgYaCIA5z5QG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m2AtBgYaCIA5z5QG .marker.cross{stroke:#333333;}#mermaid-svg-m2AtBgYaCIA5z5QG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m2AtBgYaCIA5z5QG p{margin:0;}#mermaid-svg-m2AtBgYaCIA5z5QG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-m2AtBgYaCIA5z5QG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-m2AtBgYaCIA5z5QG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-m2AtBgYaCIA5z5QG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-m2AtBgYaCIA5z5QG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-m2AtBgYaCIA5z5QG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-m2AtBgYaCIA5z5QG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-m2AtBgYaCIA5z5QG .sequenceNumber{fill:white;}#mermaid-svg-m2AtBgYaCIA5z5QG #sequencenumber{fill:#333;}#mermaid-svg-m2AtBgYaCIA5z5QG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-m2AtBgYaCIA5z5QG .messageText{fill:#333;stroke:none;}#mermaid-svg-m2AtBgYaCIA5z5QG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-m2AtBgYaCIA5z5QG .labelText,#mermaid-svg-m2AtBgYaCIA5z5QG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-m2AtBgYaCIA5z5QG .loopText,#mermaid-svg-m2AtBgYaCIA5z5QG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-m2AtBgYaCIA5z5QG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-m2AtBgYaCIA5z5QG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-m2AtBgYaCIA5z5QG .noteText,#mermaid-svg-m2AtBgYaCIA5z5QG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-m2AtBgYaCIA5z5QG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-m2AtBgYaCIA5z5QG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-m2AtBgYaCIA5z5QG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-m2AtBgYaCIA5z5QG .actorPopupMenu{position:absolute;}#mermaid-svg-m2AtBgYaCIA5z5QG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-m2AtBgYaCIA5z5QG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-m2AtBgYaCIA5z5QG .actor-man circle,#mermaid-svg-m2AtBgYaCIA5z5QG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-m2AtBgYaCIA5z5QG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 正常游戏流程 断线重连 GameState 存的是整个游戏的压缩快照,不是增量。 行动指令计算新状态更新 GameState (JSON)插入 CombatLog返回结果重连请求 + userIdSELECT * FROM GameState WHERE userId = ?最新快照 (JSON)恢复现场

** CombatLog 单独存**

  • GameState 只保留当前状态,体积小、读得快。
  • CombatLog 保留完整历史 ,用于:
    • 战斗回放功能
    • 玩家争议审计(判定是否作弊)
    • 数据分析(哪些技能使用频率高)

2.3 配置类数据

Model 主要字段 说明
Character id, name, faction, attributes (JSON) 游戏中的角色/势力定义,属性可扩展
SeasonEvent id, name, startAt, endAt, config (JSON) 每季活动配置
Policy id, name, effect (JSON) 游戏内政策/决策,即时生效或延迟生效

表共同点

它们都是由策划配置、玩家不可写的数据。设计成数据库表而非代码常量的原因是:

  • 热更新:修改活动时间无需重新部署服务
  • 多环境共享:开发、测试、生产可以用不同配置
  • 非技术人员可编辑:配合后台管理系统,策划可以直接修改

2.4 成就与联盟

Model 主要字段 说明
Alliance id, name, members (relation) 联盟/帮会实体,关联多名 User
Achievement id, code, title, description, reward 成就系统定义
UserAchievement id, userId, achievementId, unlockedAt 玩家已解锁的成就记录(联结表)

多对多关系的处理:
#mermaid-svg-XJp9RkYnZdKOVy4d{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XJp9RkYnZdKOVy4d .error-icon{fill:#552222;}#mermaid-svg-XJp9RkYnZdKOVy4d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XJp9RkYnZdKOVy4d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XJp9RkYnZdKOVy4d .marker.cross{stroke:#333333;}#mermaid-svg-XJp9RkYnZdKOVy4d svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XJp9RkYnZdKOVy4d p{margin:0;}#mermaid-svg-XJp9RkYnZdKOVy4d .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-XJp9RkYnZdKOVy4d .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-XJp9RkYnZdKOVy4d .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-XJp9RkYnZdKOVy4d .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-XJp9RkYnZdKOVy4d .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-XJp9RkYnZdKOVy4d .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XJp9RkYnZdKOVy4d .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-XJp9RkYnZdKOVy4d .node rect,#mermaid-svg-XJp9RkYnZdKOVy4d .node circle,#mermaid-svg-XJp9RkYnZdKOVy4d .node ellipse,#mermaid-svg-XJp9RkYnZdKOVy4d .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XJp9RkYnZdKOVy4d .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-XJp9RkYnZdKOVy4d .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-XJp9RkYnZdKOVy4d .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XJp9RkYnZdKOVy4d .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-XJp9RkYnZdKOVy4d .edgeLabel .label text{fill:#333;}#mermaid-svg-XJp9RkYnZdKOVy4d :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} User
UserAchievement
int
id
PK
int
userId
FK
int
achievementId
FK
datetime
unlockedAt
Achievement

UserAchievement 是一个联结表 ,它不仅仅记录"谁完成了什么成就",还带有一个额外的 unlockedAt 字段------这很常见,因为多对多关系本身往往带有关系属性(完成时间、完成时的快照等)。
#mermaid-svg-B6I1V6XF9ex8ONQ4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .error-icon{fill:#552222;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .marker.cross{stroke:#333333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 p{margin:0;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .cluster-label text{fill:#333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .cluster-label span{color:#333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .cluster-label span p{background-color:transparent;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .label text,#mermaid-svg-B6I1V6XF9ex8ONQ4 span{fill:#333;color:#333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .node rect,#mermaid-svg-B6I1V6XF9ex8ONQ4 .node circle,#mermaid-svg-B6I1V6XF9ex8ONQ4 .node ellipse,#mermaid-svg-B6I1V6XF9ex8ONQ4 .node polygon,#mermaid-svg-B6I1V6XF9ex8ONQ4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .rough-node .label text,#mermaid-svg-B6I1V6XF9ex8ONQ4 .node .label text,#mermaid-svg-B6I1V6XF9ex8ONQ4 .image-shape .label,#mermaid-svg-B6I1V6XF9ex8ONQ4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .rough-node .label,#mermaid-svg-B6I1V6XF9ex8ONQ4 .node .label,#mermaid-svg-B6I1V6XF9ex8ONQ4 .image-shape .label,#mermaid-svg-B6I1V6XF9ex8ONQ4 .icon-shape .label{text-align:center;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .node.clickable{cursor:pointer;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .arrowheadPath{fill:#333333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-B6I1V6XF9ex8ONQ4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-B6I1V6XF9ex8ONQ4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-B6I1V6XF9ex8ONQ4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .cluster text{fill:#333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .cluster span{color:#333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-B6I1V6XF9ex8ONQ4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .icon-shape,#mermaid-svg-B6I1V6XF9ex8ONQ4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .icon-shape p,#mermaid-svg-B6I1V6XF9ex8ONQ4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .icon-shape .label rect,#mermaid-svg-B6I1V6XF9ex8ONQ4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-B6I1V6XF9ex8ONQ4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-B6I1V6XF9ex8ONQ4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-B6I1V6XF9ex8ONQ4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 联盟关系
成员
成员
成员
User A
Alliance 青龙帮
User B
User C


3. 关联关系速查

关系类型 涉及模型 实现方式
一对一 User ↔ Profile profile.userId 外键
一对一 User ↔ GameState gamestate.userId 外键
多对多 User ↔ Alliance AllianceMember 中间表
多对多 User ↔ Achievement UserAchievement 联结表
一对多 GameState → CombatLog combatLog.gameStateId 外键

4. 数据库索引与查询优化

索引是性能的生命线,但不是越多越好------每个额外的索引都会拖慢写入速度。我们根据实际查询场景,有针对性地建立索引:

4.1 唯一索引(用于快速查找用户)

sql 复制代码
-- 登录时根据邮箱或用户名查找
CREATE UNIQUE INDEX idx_user_email ON "User"(email);
CREATE UNIQUE INDEX idx_user_username ON "User"(username);

4.2 复合索引(用于范围查询)

sql 复制代码
-- 查找最近活跃的玩家(用于运营活动推送)
CREATE INDEX idx_gamestate_turn_saved ON "GameState"(currentTurn, lastSavedAt);

这个复合索引的字段顺序很重要:

  • currentTurn 在前:如果你经常查询"回合数大于 N 的玩家"
  • lastSavedAt 在后:在筛选完回合数后,再按时间排序

4.3 外键索引(用于关联查询)

sql 复制代码
-- 回放战斗日志时,根据 gameStateId 快速过滤
CREATE INDEX idx_combatlog_gameStateId ON "CombatLog"(gameStateId);

4.4 JSON 字段索引(PostgreSQL 特有的 jsonb 索引)

sql 复制代码
-- 假设我们需要查询"生命值大于 100"的角色
CREATE INDEX idx_character_attributes ON "Character" USING GIN (attributes);
-- 配合查询:SELECT * FROM "Character" WHERE attributes->>'health' > '100';

💡 jsonb 的 GIN 索引可以加速任意键值查询,但会占用额外存储空间。仅在确实需要按 JSON 内部字段过滤时使用。

索引决策流程图

#mermaid-svg-Dzt76gozeQwbdqfk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Dzt76gozeQwbdqfk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Dzt76gozeQwbdqfk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Dzt76gozeQwbdqfk .error-icon{fill:#552222;}#mermaid-svg-Dzt76gozeQwbdqfk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Dzt76gozeQwbdqfk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Dzt76gozeQwbdqfk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Dzt76gozeQwbdqfk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Dzt76gozeQwbdqfk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Dzt76gozeQwbdqfk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Dzt76gozeQwbdqfk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Dzt76gozeQwbdqfk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Dzt76gozeQwbdqfk .marker.cross{stroke:#333333;}#mermaid-svg-Dzt76gozeQwbdqfk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Dzt76gozeQwbdqfk p{margin:0;}#mermaid-svg-Dzt76gozeQwbdqfk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Dzt76gozeQwbdqfk .cluster-label text{fill:#333;}#mermaid-svg-Dzt76gozeQwbdqfk .cluster-label span{color:#333;}#mermaid-svg-Dzt76gozeQwbdqfk .cluster-label span p{background-color:transparent;}#mermaid-svg-Dzt76gozeQwbdqfk .label text,#mermaid-svg-Dzt76gozeQwbdqfk span{fill:#333;color:#333;}#mermaid-svg-Dzt76gozeQwbdqfk .node rect,#mermaid-svg-Dzt76gozeQwbdqfk .node circle,#mermaid-svg-Dzt76gozeQwbdqfk .node ellipse,#mermaid-svg-Dzt76gozeQwbdqfk .node polygon,#mermaid-svg-Dzt76gozeQwbdqfk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Dzt76gozeQwbdqfk .rough-node .label text,#mermaid-svg-Dzt76gozeQwbdqfk .node .label text,#mermaid-svg-Dzt76gozeQwbdqfk .image-shape .label,#mermaid-svg-Dzt76gozeQwbdqfk .icon-shape .label{text-anchor:middle;}#mermaid-svg-Dzt76gozeQwbdqfk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Dzt76gozeQwbdqfk .rough-node .label,#mermaid-svg-Dzt76gozeQwbdqfk .node .label,#mermaid-svg-Dzt76gozeQwbdqfk .image-shape .label,#mermaid-svg-Dzt76gozeQwbdqfk .icon-shape .label{text-align:center;}#mermaid-svg-Dzt76gozeQwbdqfk .node.clickable{cursor:pointer;}#mermaid-svg-Dzt76gozeQwbdqfk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Dzt76gozeQwbdqfk .arrowheadPath{fill:#333333;}#mermaid-svg-Dzt76gozeQwbdqfk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Dzt76gozeQwbdqfk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Dzt76gozeQwbdqfk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dzt76gozeQwbdqfk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Dzt76gozeQwbdqfk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dzt76gozeQwbdqfk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Dzt76gozeQwbdqfk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Dzt76gozeQwbdqfk .cluster text{fill:#333;}#mermaid-svg-Dzt76gozeQwbdqfk .cluster span{color:#333;}#mermaid-svg-Dzt76gozeQwbdqfk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Dzt76gozeQwbdqfk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Dzt76gozeQwbdqfk rect.text{fill:none;stroke-width:0;}#mermaid-svg-Dzt76gozeQwbdqfk .icon-shape,#mermaid-svg-Dzt76gozeQwbdqfk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Dzt76gozeQwbdqfk .icon-shape p,#mermaid-svg-Dzt76gozeQwbdqfk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Dzt76gozeQwbdqfk .icon-shape .label rect,#mermaid-svg-Dzt76gozeQwbdqfk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Dzt76gozeQwbdqfk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Dzt76gozeQwbdqfk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Dzt76gozeQwbdqfk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否







有查询慢的问题?
暂不建索引
该列是否经常作为 WHERE 条件?
是否多个列一起作为条件?
建复合索引

注意列顺序
建单列索引
写入频率高吗?
平衡读写,

评估是否值得
放心建索引


5. 迁移与种子数据

5.1 迁移流程

代码即架构。当模型发生变化时,遵循以下流程:
#mermaid-svg-45Y8F583jEOVjzQM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-45Y8F583jEOVjzQM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-45Y8F583jEOVjzQM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-45Y8F583jEOVjzQM .error-icon{fill:#552222;}#mermaid-svg-45Y8F583jEOVjzQM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-45Y8F583jEOVjzQM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-45Y8F583jEOVjzQM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-45Y8F583jEOVjzQM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-45Y8F583jEOVjzQM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-45Y8F583jEOVjzQM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-45Y8F583jEOVjzQM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-45Y8F583jEOVjzQM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-45Y8F583jEOVjzQM .marker.cross{stroke:#333333;}#mermaid-svg-45Y8F583jEOVjzQM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-45Y8F583jEOVjzQM p{margin:0;}#mermaid-svg-45Y8F583jEOVjzQM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-45Y8F583jEOVjzQM .cluster-label text{fill:#333;}#mermaid-svg-45Y8F583jEOVjzQM .cluster-label span{color:#333;}#mermaid-svg-45Y8F583jEOVjzQM .cluster-label span p{background-color:transparent;}#mermaid-svg-45Y8F583jEOVjzQM .label text,#mermaid-svg-45Y8F583jEOVjzQM span{fill:#333;color:#333;}#mermaid-svg-45Y8F583jEOVjzQM .node rect,#mermaid-svg-45Y8F583jEOVjzQM .node circle,#mermaid-svg-45Y8F583jEOVjzQM .node ellipse,#mermaid-svg-45Y8F583jEOVjzQM .node polygon,#mermaid-svg-45Y8F583jEOVjzQM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-45Y8F583jEOVjzQM .rough-node .label text,#mermaid-svg-45Y8F583jEOVjzQM .node .label text,#mermaid-svg-45Y8F583jEOVjzQM .image-shape .label,#mermaid-svg-45Y8F583jEOVjzQM .icon-shape .label{text-anchor:middle;}#mermaid-svg-45Y8F583jEOVjzQM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-45Y8F583jEOVjzQM .rough-node .label,#mermaid-svg-45Y8F583jEOVjzQM .node .label,#mermaid-svg-45Y8F583jEOVjzQM .image-shape .label,#mermaid-svg-45Y8F583jEOVjzQM .icon-shape .label{text-align:center;}#mermaid-svg-45Y8F583jEOVjzQM .node.clickable{cursor:pointer;}#mermaid-svg-45Y8F583jEOVjzQM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-45Y8F583jEOVjzQM .arrowheadPath{fill:#333333;}#mermaid-svg-45Y8F583jEOVjzQM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-45Y8F583jEOVjzQM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-45Y8F583jEOVjzQM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-45Y8F583jEOVjzQM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-45Y8F583jEOVjzQM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-45Y8F583jEOVjzQM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-45Y8F583jEOVjzQM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-45Y8F583jEOVjzQM .cluster text{fill:#333;}#mermaid-svg-45Y8F583jEOVjzQM .cluster span{color:#333;}#mermaid-svg-45Y8F583jEOVjzQM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-45Y8F583jEOVjzQM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-45Y8F583jEOVjzQM rect.text{fill:none;stroke-width:0;}#mermaid-svg-45Y8F583jEOVjzQM .icon-shape,#mermaid-svg-45Y8F583jEOVjzQM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-45Y8F583jEOVjzQM .icon-shape p,#mermaid-svg-45Y8F583jEOVjzQM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-45Y8F583jEOVjzQM .icon-shape .label rect,#mermaid-svg-45Y8F583jEOVjzQM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-45Y8F583jEOVjzQM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-45Y8F583jEOVjzQM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-45Y8F583jEOVjzQM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 修改 schema.prisma
npx prisma migrate dev --name 描述
自动生成 SQL 迁移文件
应用到开发数据库
重新生成 Prisma Client
提交 migration 到 Git

关键原则:

  • 迁移文件必须提交到版本控制,保证团队和环境一致
  • 生产环境的迁移由 CI/CD 流程执行,不要在服务器上手动改表

5.2 种子数据

prisma/seed.ts 负责初始化两类数据:

类型 内容 目的
基础配置 Character、Policy、SeasonEvent 让新环境能直接运行游戏逻辑
测试数据 演示用的 User、GameState 本地开发时能看到非空界面

执行方式:

bash 复制代码
npx prisma db seed

6. 兼容性与演进

6.1 命名映射

使用 @@map@map 将 Prisma 模型名映射到实际的数据库表名:

prisma 复制代码
model User {
  id        Int    @id @map("user_id")
  username  String @map("user_name")
  
  @@map("t_user")  // 表名统一加前缀
}

注意

  • PostgreSQL 默认大小写敏感,Useruser 是不同的表
  • 不同语言的命名习惯不同(TypeScript 喜欢 PascalCase,SQL 喜欢 snake_case)

6.2 类型安全的业务代码

typescript 复制代码
// ✅ 直接从 prisma/client 导入类型
import { User, GameState } from '@prisma/client';

async function saveGame(userId: number, state: GameState) {
  // state 参数有完整的类型提示
  return prisma.gameState.upsert({
    where: { userId },
    update: { ...state, lastSavedAt: new Date() },
    create: { userId, ...state },
  });
}

6.3 添加新字段的注意事项

prisma 复制代码
model User {
  id        Int      @id @default(autoincrement())
  username  String
  // 新加字段:必须提供默认值或标记为可选
  vipLevel  Int      @default(0)      // ✅ 有默认值,旧数据自动补0
  inviteCode String?                   // ✅ 可选字段,旧数据为 null
  // inviteCode String                   // ❌ 不行!旧数据没有这个字段
}

当执行 migrate dev 时,Prisma 会生成 ALTER TABLE ... ADD COLUMN 语句。如果新字段没有默认值且不为空,数据库不知道如何填充已有的 100 万行记录,迁移就会失败。


7. 本章小结

要点 说明
模型即代码 Prisma 模型 → TypeScript 类型 + SQL 表,三者同步
索引按需创建 根据实际查询场景选择单列、复合或 JSON 索引
种子数据分离 配置数据与测试数据分开,方便不同环境使用
迁移要可回放 所有迁移文件提交 Git,保证环境一致性
新增字段要兜底 提供 @default 或标记为可选,避免旧数据迁移失败

后续章节将基于本章的数据模型,展开业务逻辑的实现细节。理解这些模型之间的关系和设计意图,是阅读后续内容的基础。

相关推荐
心疼你的一切3 小时前
高效内容生产:如何实现规模化创作
大数据·人工智能·ai·ai编程·ai写作
QYR-分析3 小时前
智能化重构仓储物流:仓储人形机器人行业全景解析
人工智能·重构·机器人
AI 小老六3 小时前
Claude Code 如何压缩上下文:Microcompact、Prompt Cache 与 cache_edits 工程拆解
数据库·人工智能·ai·语言模型·架构·系统架构
侃谈科技圈3 小时前
多门店数据孤岛破局:零售连锁一体化系统2026选型
人工智能·零售
lqqjuly4 小时前
注意力机制完全详解
人工智能·语言模型
数据科学小丫4 小时前
特征工程处理
人工智能·算法·机器学习
WooaiJava4 小时前
即将到达的AI时代——Claude Code
人工智能
风落无尘4 小时前
第十章《多模态与具身》 完整学习资料
人工智能·语言模型·aigc