03. 验证数据并创建只读账号

1. 本章目标

导入命令没有报错,只能说明工具完成了流程,不能证明数据与应用预期一致。本章验证表、规模、结构和关键数据语义,然后建立 airportdb_training 与只读应用账号。

2. 确认实际 schema 名

进入 MySQL:

sudo mysql

查看数据库:

SHOW DATABASES;

以下示例使用 airportdb_training。如果导入结果叫 airportdb,有两个选择:

复制五千多万行数据会消耗大量时间和磁盘。不要直接执行一个未经评估的复制命令。空间不足时,第一阶段可以只有一个 airportdb_training,同时保留官方 ZIP 和重新导入步骤。

下面假设目标库已经是 airportdb_training:

USE airportdb_training;

3. 验证 14 张表

SHOW TABLES;
SELECT COUNT(*) AS table_count
FROM information_schema.tables
WHERE table_schema = 'airportdb_training'
  AND table_type = 'BASE TABLE';

官方资料描述 AirportDB 有 14 张表。下载版本和修复脚本可能不同,所以需要记录实际结果,不能为了凑数而删除或创建表。

查看表状态:

SHOW TABLE STATUS FROM airportdb_training;

InnoDB 的 TABLE_ROWS 是估算值,适合快速了解规模,不等于精确 COUNT(*)。

4. 检查关键表结构

SHOW CREATE TABLE airportdb_training.flight\G
SHOW CREATE TABLE airportdb_training.booking\G
SHOW CREATE TABLE airportdb_training.airport_geo\G
SHOW CREATE TABLE airportdb_training.weatherdata\G

重点记录:

应用中的 Sequelize Model 必须映射现有结构,禁止用 sequelize.sync({ force: true }) 或 sequelize.sync({ alter: true }) 修改样本库。

5. 检查数据规模

先用表状态估算,随后仅对必须记录的关键表执行精确计数:

SELECT COUNT(*) AS airport_count FROM airport;
SELECT COUNT(*) AS flight_count FROM flight;
SELECT COUNT(*) AS booking_count FROM booking;
SELECT COUNT(*) AS weather_count FROM weatherdata;

booking 的精确计数可能需要较长时间。第一次执行前可在另一个会话观察:

SHOW FULL PROCESSLIST;

如果只是判断数据是否存在,优先使用有限查询:

SELECT * FROM booking LIMIT 5;

不要在应用的 /health 中执行大表 COUNT(*)。健康检查只验证数据库能否响应轻量连接或查询。

6. 验证日期和坐标

检查航班日期范围:

SELECT MIN(departure) AS min_departure,
       MAX(departure) AS max_departure
FROM flight;

检查天气时间范围:

SELECT MIN(log_date) AS min_log_date,
       MAX(log_date) AS max_log_date
FROM weatherdata;

实际列名以 SHOW CREATE TABLE 为准。如果示例列不存在,先根据表结构调整,不要猜测并修改数据库。

检查机场坐标样本:

SELECT *
FROM airport_geo
LIMIT 10;

若包含 POINT,使用与实际列名对应的 ST_X()、ST_Y() 检查。必须通过已知机场验证 X/Y 分别代表经度还是纬度,不要仅凭经验假设。

7. 创建只读应用账号

7.1 为什么单独创建账号

后端只需要查询数据。若 .env 泄露或程序有缺陷,只读权限可以降低删除或修改样本数据的风险。Node.js 应用绝不能使用 root。

生成一个强随机密码。可以在服务器上执行:

openssl rand -base64 32

进入 MySQL,以实际密码替换 <强随机密码>:

CREATE USER 'airport_app'@'127.0.0.1'
IDENTIFIED BY '<强随机密码>';

GRANT SELECT
ON airportdb_training.*
TO 'airport_app'@'127.0.0.1';

SHOW GRANTS FOR 'airport_app'@'127.0.0.1';

MySQL 把 'airport_app'@'127.0.0.1' 与 'airport_app'@'localhost' 视为不同账号。本项目 .env 使用 DB_HOST=127.0.0.1,所以创建相同 host 部分的账号最清楚。

如果账号已经存在,应先检查权限,不要反复执行 CREATE USER。更换密码使用:

ALTER USER 'airport_app'@'127.0.0.1'
IDENTIFIED BY '<新的强随机密码>';

8. 验证账号确实只读

退出管理员会话:

EXIT;

使用应用账号连接:

mysql -h 127.0.0.1 -u airport_app -p airportdb_training

验证读取:

SELECT COUNT(*) FROM airport;
SHOW GRANTS;

在训练环境中验证写入被拒绝,可以执行一个不会成功的事务:

START TRANSACTION;
UPDATE airport SET name = name LIMIT 1;
ROLLBACK;

预期 UPDATE 返回权限错误。不要为了测试权限而授予写权限。

9. 配置项目环境变量

在项目目录:

cd /var/www/airport-operations-explorer
cp .env.example server/.env
chmod 600 server/.env

编辑 server/.env:

NODE_ENV=production
HOST=127.0.0.1
PORT=3000
CLIENT_ORIGIN=https://airport.cxhair.cn
LOG_LEVEL=info

DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=airportdb_training
DB_USER=airport_app
DB_PASSWORD=<强随机密码>
DB_POOL_MAX=10
DB_HEALTH_TIMEOUT_MS=2000

server/.env 已被 .gitignore 排除。不要把内容粘贴到 issue、日志或 Git 提交中。

10. 使用项目健康检查验证

安装并构建后端:

npm ci
npm run build -w server
npm start -w server

在另一个 SSH 会话:

curl -i http://127.0.0.1:3000/health

正常响应应是 HTTP 200,并包含:

{
  "data": {
    "status": "ok",
    "database": {
      "status": "up"
    }
  }
}

如果返回 503:

  1. 检查 server/.env 是否位于正确目录;
  2. 手工使用相同账号连接 MySQL;
  3. 检查 DB_HOST、DB_NAME 和账号 host 部分;
  4. 查看后端日志,但不要把密码打印出来。

验证后按 Ctrl+C 停止前台进程。第 5 章会用 PM2 正式管理它。

11. 本章验收

下一章:DNS 与 Certbot 子域名证书