百游棋牌源代码开发搭建教程(二):PostgreSQL安装、业务表创建与版本化迁移

文章摘要: 本篇实际准备教学数据库,说明容器启动、数据库登录、业务账号、连接字符串、表结构和迁移脚本,逐步建立账号、房间、事件与积分记录,并验证迁移重复执行、权限范围和完整服务就绪状态。

文章目录


前言

上一篇已经确认Node服务能够监听本机端口,但就绪检查仍然返回503。这是因为业务数据库尚未接入。本篇要把"进程启动"推进到"数据库结构已经满足服务要求",让下一篇的注册登录有真实保存位置。

使用本系列定义的PostgreSQL结构,不表示百游原版必须使用该数据库。选择它是为了在同一套教程中清楚展示事务、唯一约束和结构迁移。读者如果持有其他数据库的旧源码,不能直接把本篇SQL导入原业务库。

所有操作先在BaiyouLab独立测试环境完成。本文提供的初始迁移只能在新库首次应用,不用于覆盖客户已有账号或牌局数据。

一、准备Docker与工作目录

安装适合操作系统的Docker环境并启动,再检查:

powershell 复制代码
docker version
docker compose version
Set-Location (Join-Path $env:USERPROFILE 'BaiyouLab\01-working')
Test-Path -LiteralPath '.\compose.yaml'

docker version需要同时出现客户端和服务端信息。只有客户端版本、随后提示无法连接引擎时,先启动Docker服务或检查运行环境,不要修改数据库端口。Windows上的容器后端与虚拟化条件按Docker当前文档准备。

本篇需要Linux容器模式。若镜像无法启动,先核对容器模式、镜像架构和磁盘空间。数据库会持续写入数据卷,工作盘空间不足可能表现为启动失败或后续写入错误。

已经通过原生方式安装PostgreSQL的读者,可以使用独立的新库与账号代替容器;后续SQL与迁移步骤不变,但启动和备份路径需要相应调整。

二、阅读数据库容器配置

附件compose.yaml内容如下:

yaml 复制代码
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_DB: baiyou
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set a database password}
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d baiyou"]
      interval: 5s
      timeout: 3s
      retries: 12
volumes:
  db_data:

端口仅绑定宿主机回环地址,当前开发电脑可以连接,外部设备不能直接访问数据库。客户端手机应访问游戏服务,不应直接开放数据库给手机。

命名卷db_data保存数据,重建容器不会自动清空它。postgres:17是教学主版本标签,实际镜像补丁可能随拉取时间变化;部署候选版本应记录完整镜像标识,生产维护再按计划更新。

pg_isready只检查数据库是否接受连接,不证明表已经建立或业务账号权限正确。稍后还要执行SQL与应用就绪检查。Compose支持结合健康条件控制依赖启动,但应用仍需处理运行中数据库掉线。Docker依赖启动说明

三、设置管理员密码并启动数据库

为Compose创建本地文件.env.db,只放一项:

dotenv 复制代码
POSTGRES_PASSWORD=在此填入自行生成的强密码

这里的中文说明必须替换,不要直接作为密码使用。该文件不提交仓库,也不放进文章附件。若密码包含环境文件特殊字符,应按对应语法正确引用;不要因为连接失败就改成固定弱密码。

powershell 复制代码
docker compose --env-file .env.db up -d db
docker compose --env-file .env.db ps
docker compose --env-file .env.db logs --tail 50 db

等状态变为健康后进入数据库控制台:

powershell 复制代码
docker compose --env-file .env.db exec db psql -U postgres -d baiyou

进入后执行:

sql 复制代码
SELECT current_database(), current_user, version();
SHOW server_encoding;
SHOW timezone;

应连接到baiyou库,当前管理角色为postgres,编码为UTF8。控制台里的反斜杠命令属于psql,不在PowerShell中执行。退出使用\q

四、创建独立业务账号

仍在psql中执行:

sql 复制代码
CREATE ROLE baiyou_app LOGIN;
\password baiyou_app
GRANT CONNECT ON DATABASE baiyou TO baiyou_app;
GRANT USAGE ON SCHEMA public TO baiyou_app;

\password会提示输入新密码,避免把明文写进SQL文件。该账号用来处理日常注册、房间与查询;表结构由迁移账号建立。不要把数据库超级用户凭据填入客户端配置。

此时业务表尚不存在,所以先不执行表授权。若提示角色已存在,说明不是首次运行;检查已有角色,不要盲目删除重建。后续需要修改密码时使用明确的密码轮换操作。

修改.env.db只改变容器初始化输入,不会自动修改已有数据卷内的数据库密码。遇到"我改了配置但密码不生效",先确认数据卷是否已经初始化,而不是删除整个数据库目录。

五、完整认识初始业务表

附件sql/001-initial.sql定义本系列实际使用的结构:

sql 复制代码
CREATE TABLE users (
  id uuid PRIMARY KEY,
  username text NOT NULL UNIQUE,
  password_hash text NOT NULL,
  disabled boolean NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE sessions (
  token_hash text PRIMARY KEY,
  user_id uuid NOT NULL REFERENCES users(id),
  expires_at timestamptz NOT NULL
);
CREATE INDEX sessions_expiry_idx ON sessions(expires_at);
CREATE TABLE clubs (
  id uuid PRIMARY KEY,
  name text NOT NULL,
  owner_id uuid NOT NULL REFERENCES users(id),
  version integer NOT NULL DEFAULT 1
);
CREATE TABLE club_members (
  club_id uuid NOT NULL REFERENCES clubs(id),
  user_id uuid NOT NULL REFERENCES users(id),
  role text NOT NULL CHECK (role IN ('owner','member')),
  PRIMARY KEY(club_id,user_id)
);
CREATE TABLE rooms (
  id uuid PRIMARY KEY,
  club_id uuid REFERENCES clubs(id),
  owner_id uuid NOT NULL REFERENCES users(id),
  status text NOT NULL CHECK(status IN ('WAITING','PLAYING','FINISHED')),
  rule_version text NOT NULL DEFAULT 'G1',
  seq integer NOT NULL DEFAULT 0,
  state jsonb NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE room_members (
  room_id uuid NOT NULL REFERENCES rooms(id),
  user_id uuid NOT NULL REFERENCES users(id),
  seat smallint NOT NULL CHECK(seat IN (0,1)),
  ready boolean NOT NULL DEFAULT false,
  PRIMARY KEY(room_id,user_id),
  UNIQUE(room_id,seat)
);
CREATE TABLE command_results (
  user_id uuid NOT NULL REFERENCES users(id),
  request_id uuid NOT NULL,
  fingerprint text NOT NULL,
  response jsonb NOT NULL,
  PRIMARY KEY(user_id,request_id)
);
CREATE TABLE room_events (
  room_id uuid NOT NULL REFERENCES rooms(id),
  seq integer NOT NULL,
  event jsonb NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY(room_id,seq)
);
CREATE TABLE settlements (
  room_id uuid PRIMARY KEY REFERENCES rooms(id),
  result jsonb NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE score_entries (
  room_id uuid NOT NULL REFERENCES settlements(room_id),
  user_id uuid NOT NULL REFERENCES users(id),
  delta integer NOT NULL,
  PRIMARY KEY(room_id,user_id)
);
CREATE TABLE audit_events (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  actor_id uuid NOT NULL REFERENCES users(id),
  action text NOT NULL,
  target_id uuid NOT NULL,
  detail jsonb NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

users保存账号,sessions只保存令牌摘要与过期时间。rooms保存规则版本、最新序号与完整教学棋盘;room_members同时限制同一玩家不重复加入、同一座位不被两人占用。唯一约束是并发保护的一部分,不能只靠客户端禁止双击。

command_results以账号和请求编号去重;room_events以房间和序号排序;settlements限制每房间一局的教学结算只有一份;score_entries记录不可兑换现金的教学积分。这里每个房间只运行一局,多局产品应增加独立单局编号,不能沿用房间编号充当所有结算键。

clubsclub_members供第九篇权限实验使用,audit_events记录管理变更。初始结构不包含商业房卡购买、充值或现金兑换,也不把缺失业务隐藏在"完整后台"四个字里。

六、配置迁移与运行连接

修改工作目录.env,增加或填写:

dotenv 复制代码
DATABASE_URL=postgresql://baiyou_app:业务密码@127.0.0.1:5432/baiyou
MIGRATION_DATABASE_URL=postgresql://postgres:管理员密码@127.0.0.1:5432/baiyou

用户名和密码位置都是待替换说明。如果密码含@:/等字符,必须对用户名、密码部分进行URL百分号编码,不能对整个连接字符串编码。错误编码常导致程序把密码一部分解释成主机或端口。

迁移账号仅供迁移命令使用。完成迁移后,可以从日常运行环境移除管理员连接,单独在受控迁移环境提供。不要把整条URL输出到日志验证,使用一次只返回库名和用户的查询更合适。

七、连接池与事务必须使用同一个连接

核对src/db.mjs

javascript 复制代码
import pg from 'pg';

export function createPool(connectionString) {
  return new pg.Pool({
    connectionString, max: 10,
    connectionTimeoutMillis: 3000,
    idleTimeoutMillis: 30000,
    statement_timeout: 5000
  });
}

export async function inTransaction(pool, work) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const result = await work(client);
    await client.query('COMMIT');
    return result;
  } catch (error) {
    try { await client.query('ROLLBACK'); } catch { /* preserve original error */ }
    throw error;
  } finally {
    client.release();
  }
}

连接池限制连接数量并设置等待与语句超时。数值是本地教学起点,不是容量承诺。某些重型迁移可能需要单独调整超时,不能通过无限等待掩盖阻塞。

inTransaction先借出一个客户端,随后BEGIN、业务查询、COMMIT或ROLLBACK都使用这个客户端,最后归还。不要用pool.query('BEGIN')后再多次pool.query冒充事务,因为这些查询可能分配到不同连接。node-postgres事务说明

回滚也可能因连接中断失败,因此保留原始错误。业务重试是否允许,要由请求幂等与事务结果决定,不能见到任何异常就重新执行一次。

八、执行带校验值的迁移

核对scripts/migrate.mjs完整代码:

javascript 复制代码
import { readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { createPool, inTransaction } from '../src/db.mjs';
const url = process.env.MIGRATION_DATABASE_URL || process.env.DATABASE_URL;
if (!url || url.includes('REPLACE_ME')) throw new Error('Set migration database URL');
const pool = createPool(url);
try {
  await inTransaction(pool, async client => {
    await client.query('SELECT pg_advisory_xact_lock(98112026)');
    await client.query(`CREATE TABLE IF NOT EXISTS schema_migrations (
      name text PRIMARY KEY, checksum text NOT NULL, applied_at timestamptz NOT NULL DEFAULT now())`);
    const name = '001-initial.sql';
    const sql = await readFile(new URL('../sql/' + name, import.meta.url), 'utf8');
    const checksum = createHash('sha256').update(sql).digest('hex');
    const old = await client.query('SELECT checksum FROM schema_migrations WHERE name=$1', [name]);
    if (old.rowCount) {
      if (old.rows[0].checksum !== checksum) throw new Error('Applied migration was modified');
      console.log('Already applied:', name);
      return;
    }
    await client.query(sql);
    await client.query('INSERT INTO schema_migrations(name,checksum) VALUES($1,$2)', [name, checksum]);
    console.log('Applied:', name);
  });
} finally { await pool.end(); }

脚本建立迁移登记表,保存文件名与SHA256摘要。重复运行时,已经应用且内容相同的迁移跳过;文件被修改则拒绝继续,避免同名版本在不同环境代表不同结构。

事务级咨询锁用于协调同时执行的迁移。它只有所有迁移入口使用相同锁约定时才有效,不是阻止任何管理员手工改库的全局保护。正式项目有多份迁移时,按明确排序依次执行,并保持已经应用的文件不可改写。

powershell 复制代码
pnpm run migrate
pnpm run migrate

首次预期显示Applied: 001-initial.sql,第二次显示Already applied: 001-initial.sql。若第一次失败,检查最先出现的SQL错误,事务应回滚本次结构变更,不要只从中间一条语句继续运行。

九、授予业务所需权限

迁移成功后,再进入psql执行:

sql 复制代码
GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA public TO baiyou_app;
GRANT USAGE,SELECT ON ALL SEQUENCES IN SCHEMA public TO baiyou_app;
REVOKE INSERT,UPDATE,DELETE ON schema_migrations FROM baiyou_app;
REVOKE UPDATE,DELETE ON audit_events,room_events,score_entries,settlements FROM baiyou_app;

这是本教学表集合的授权方式。迁移登记不让业务修改,事件和结果采用追加记录。新增表后需要按新版本重新审核权限,不能把本地演示授权原封不动推广到包含其他业务的共享数据库。

用业务账号实际连接核对表可读,而不是只观察GRANT成功。若使用psql通过宿主端口连接,会提示输入对应密码:

powershell 复制代码
docker compose --env-file .env.db exec db psql -h 127.0.0.1 -U baiyou_app -d baiyou -W
sql 复制代码
SELECT current_user;
SELECT name,applied_at FROM schema_migrations;
SELECT count(*) FROM users;

新库用户数应为零。角色、库名和迁移记录一致后,才进入应用层验证。

十、启动完整服务并检查就绪

先在第一篇的服务终端按Ctrl+C,然后在工作目录执行:

powershell 复制代码
pnpm start

第二个终端查询:

powershell 复制代码
Invoke-RestMethod 'http://127.0.0.1:3000/health/live'
Invoke-RestMethod 'http://127.0.0.1:3000/health/ready'

预期分别返回aliveready。此时完整入口会注册下一篇的账号接口,但我们先不创建用户。就绪检查确认了连接与初始迁移标记,仍不能替代全部业务表权限和并发测试。

返回503时,先看错误类别:未配置数据库、数据库不可访问、迁移不存在。然后依次检查地址、密码、端口、库名和迁移账号是否连接了另一个库。不要一开始就把运行账号改成超级用户。

十一、常见数据库错误的排查顺序

连接被拒绝时,先检查容器和端口;密码认证失败时,核对账号、数据卷是否旧初始化、密码编码;表不存在时,确认当前库与迁移记录;权限不足时,查询具体对象授权;连接池等待时,检查未释放连接和长事务。

如果SQL报重复表,而迁移登记为空,可能有人手工执行了部分建表。先核对环境状态,保留必要数据,再决定在新的空测试库重建;不要删除已有客户数据库来"快速解决"。

数据库版本也要记录。主版本升级涉及数据兼容与迁移流程,不能简单替换容器标签后继续挂载旧数据目录。PostgreSQL官方版本与升级说明应作为核对依据。PostgreSQL版本政策

十二、把数据库故障分层定位

1. 连接失败不等于数据库内容损坏

应用报连接错误时,先从容器状态、端口、账号认证、数据库名称、结构迁移和业务权限六层逐项检查。容器没有运行时,修改SQL没有意义;密码错误时,重新建表也不会改善;表存在但运行账号无权访问时,换成管理员启动只能暂时掩盖授权缺口,不能作为最终修复。

建议把检查结果写成一行记录,例如"容器健康,回环端口可达,管理员可登录,应用账号认证失败"。这比粘贴几百行日志更容易确定下一步。确认一个条件后再进入下一层,不同时更改密码、端口和库名,否则即使恢复,也无法解释哪一步起了作用。

2. 理解容器名称与数据库名称

Compose里的db是服务名,baiyou是数据库名,baiyou_app是数据库角色,db_data是数据卷名。它们恰好都出现在搭建过程中,但不能互相替换。Node在宿主机运行时访问127.0.0.1映射端口;如果以后把Node也放进同一Compose网络,应使用对应服务名访问容器内部端口,不能继续把容器内的127.0.0.1当成另一台容器。

本系列当前只把数据库放进容器,服务端运行在宿主机,所以连接串保持前文形式。不要提前修改成db主机名后,再通过添加系统hosts来绕过解析错误。先确认进程实际运行位置,网络地址才能选对。

3. 迁移校验失败时保留现场

迁移脚本记录文件名和校验值。已经应用过的001文件被编辑后再次执行,会报告不一致。这不是要求你删除schema_migrations里的记录,而是在提醒数据库历史与当前文件不再对应。删除记录后强行重跑,既可能遇到表已存在,也可能破坏对历史结构的判断。

正确流程是保留已经应用的文件,新增后续迁移并明确升级步骤。本教学脚本只读取001,后续扩展还要增加有序迁移发现与执行逻辑;不能仅在sql目录放一个002文件就认为现有脚本会自动运行它。文章中的运行能力应与实际脚本保持一致。

4. 检查权限时使用真正的运行账号

管理员成功插入一行,只能证明管理员有权限。应用账号还要能够插入users、sessions、房间和相关事件,并读取迁移标记;审计表的自增编号还涉及序列权限。第二篇的授权语句把这些需要明确写出来,避免等第九篇才发现创建俱乐部时生成审计编号失败。

同时,运行账号不需要修改已经保存的结算与审计记录。限制更新和删除,有助于减少错误代码造成的数据改写范围,但这不意味着数据库管理员也无法修改,更不能把普通审计表描述成密码学意义上的不可篡改存储。权限设计与审计可信程度应分别说明。

5. 做一次暂停数据库的观察实验

先保持完整服务运行,在无人进行其他验收的本地环境暂停数据库容器,再请求live和ready。前者通常仍可回答进程存活,后者应报告数据库或迁移不可用。随后恢复数据库,等待连接可用,再查ready和登录接口。不要使用删除数据卷的方式模拟断线,暂停网络服务已经足够。

这次练习能帮助理解进程健康与业务就绪的区别。不过它不等同于事务中断或磁盘故障测试,无法证明所有故障都可以自动恢复。后续需要分别模拟请求进行中的数据库断连,并检查调用方是否得到可识别失败、连接池是否能够重新建立连接。

6. 在进入账号篇前核对数据起点

新迁移完成时,业务表存在但没有教学玩家,属于正常结果。下一篇注册才会写入账号。不要为了让数据库看起来"有内容",导入网上来源不明的用户表或旧游戏备份;字段、密码格式和身份编号可能完全不同,反而使教程无法复现。

如果反复练习留下了多组数据,给本次实验记录准确的库名、账号和房间编号即可。需要重新开始时优先建立新的独立测试库,并完整执行迁移和授权。只有明确确认旧库属于本次教学且无需保留时,才考虑清理,不把重建数据库作为所有错误的通用处理方式。

补充检查应用进程是否仍持有旧连接配置。修改环境文件后重新启动,再核对实际连接的数据库名称与角色。不要在同一终端里同时保留两个不同数据库地址来源,否则迁移连接正确、运行连接错误,仍可能出现表已建好却一直未就绪的现象。

十三、本篇验收与下一步

完成后应能够证明:数据库健康;管理员与业务角色分离;初始迁移已登记;第二次迁移没有重复执行;业务账号可访问所需表;事件与迁移表权限符合设计;完整服务就绪返回200。

保存容器镜像标识、数据库版本和迁移摘要,备份配置时去除真实密码。

下一篇将创建第一个玩家账号,完成密码哈希、会话令牌、登录验证与退出失效,并通过真实HTTP请求验证正反两条路径。

相关推荐
企业解惑小助手1 小时前
如何禁止文字复制?企业文档防复制项目需求拆解方案
运维·数据库·安全
蓝速科技1 小时前
酒店门店 AI 数字人前台场景适配与落地指南
大数据·运维·数据结构·数据库·人工智能·科技
oradh2 小时前
Oracle UNDO 数据文件丢失处理(案例一)
数据库·oracle
richard_first2 小时前
Transformer与大语言模型:第18章 向量数据库
数据库·人工智能·自然语言处理·transformer·embedding
El Shaddai.plus2 小时前
达梦数据库的执行计划操作符介绍
数据库·oracle
神仙别闹2 小时前
基于 QT(C++)开发的地铁换乘系统
数据库·c++·qt
FairGuard手游加固3 小时前
Unity小游戏加密:global-metadata.dat与AssetBundle保护
游戏·unity·游戏引擎
Token掘金室3 小时前
Function Calling完整调用教程
大数据·数据库·人工智能
钱栈up3 小时前
qoder CLI 1.0.45 本地小说生成工作流搭建:Feature Gate 配置与长文本断连排查从死锁到全流程跑通:Python德州扑克项目的优化
java·前端·数据库