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

数据模型设计

本章将详细介绍项目的数据模型设计。我选择了 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 玩家账户信息,关联 Profile 与 GameState
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 默认大小写敏感,User 和 user 是不同的表
  • 不同语言的命名习惯不同(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 或标记为可选,避免旧数据迁移失败

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

相关推荐
AI搅拌机21 分钟前
ComfyUI管理大师:安全稳定升级+切换指定版本!
人工智能
浅安的邂逅22 分钟前
20929-OpenAI 一天踩三脚急刹:暂停前沿训练、叫停 Astra、披露越权访问澳政府网站
人工智能·大模型·ai编程·行业动态·ai日报
Qyr9933 分钟前
2026-2032直接芯片液冷板市场爆发式增长:AI算力浪潮下的热管理核心赛道
大数据·人工智能
微学AI35 分钟前
不让每一步都调用最贵模型:用蓝耘智能路由改造自主式研究 Agent
数据库·人工智能·蓝耘
a努力。36 分钟前
百毫秒搜索架构:高可用电商系统实战
人工智能
蓝色的风-202638 分钟前
国产算力双雄对决:曙光8000十万卡集群 vs 华为昇腾950超节点深度解析
大数据·人工智能·自然语言处理
xcLeigh1 小时前
AI 编程的未来趋势:2025-2026 年你必须关注的六大技术方向
人工智能·ai·ai编程
xcLeigh1 小时前
88%在用,不到10%完成规模化部署——AI Agent落地差在哪里
人工智能
一见已难忘1 小时前
产业AI落地技术解析:边缘部署、工业视觉检测与多传感器融合预警的工程实践(燎原:我看见的智能中国)
人工智能·计算机视觉·视觉检测
小燕子~~1 小时前
Photoshop2026新版 AI 功能实测,看这一篇就够了
图像处理·人工智能·ai·aigc·photoshop