江西医务康复系统:MySQL 部署前问题逐项说明

本地 SQLite → 阿里云 MySQL|依据 2026-09-08 源码核查|本文为问题说明,尚未执行修复或生产部署

这些问题分为三类:启动可能失败、启动成功但部分业务异常、运行一段时间或重启后才暴露问题。SQLite 数据迁移是独立工作,修改数据库配置不会搬迁数据。

验证边界:ID 重复已经通过两个独立进程复现;007 迁移拆分问题已经复现。其他 MySQL 问题依据源码核查,尚未完成 MySQL 实例上的完整回归。附件覆盖风险依据代码执行顺序推导,没有使用真实文件执行破坏性复现。

问题总览

编号 问题 主要后果
1 MySQL 建表使用未引用的保留字 首次初始化失败
2 迁移 SQL 拆分不兼容行内注释 第 007 个迁移可能失败
3 收藏使用 SQLite 专用 SQL MySQL 下收藏功能失败
4 后端编译没有复制迁移 SQL 空库启动失败,旧库可能漏升级
5 事务连接与实际查询连接不一致 回滚无法撤销已写入数据
6 ID 使用内存计数 同日重启或多进程出现重复 ID
7 附件先写文件,再写数据库 ID 冲突可能覆盖旧附件
8 配置切换不搬迁数据 MySQL 看不到原 SQLite 数据
9 监听地址与默认凭据 实际监听范围及生产身份配置不符合预期

1. 建表使用了 MySQL 保留字

源码位置:server/database/migrations/mysql/001_init.sql

当前 system_settings 表包含以下列定义:

key VARCHAR(64) PRIMARY KEY

KEY 是 MySQL 保留字,作为列名使用时需要引用。MySQL 官方说明

触发过程: 首次启动自动执行建表 SQL,前面的表可能已经创建,到这张表时出现语法错误,初始化中断,后端无法完成正常启动。

MySQL 建表操作可能隐式提交,前面创建的表不一定随失败撤销,因此数据库可能停留在“初始化了一部分”的状态。

处理方式: 给列名加反引号,或者改成明确的字段名,例如 setting_key,并同步修改所有引用。已有数据库不能直接随意改名,需要明确的结构迁移。

验收: 空库首次初始化成功;再次启动不重复执行;已有库升级成功。不能只验证修改后的单条 SQL。

2. 迁移 SQL 拆分方法不兼容当前文件写法

源码位置:server/database/index.tsserver/database/migrations/mysql/007_home_dashboard.sql

当前程序先将 SQL 文件拆成单条语句:

sql.split(/;\s*(?:\r?\n|$)/)

这个规则要求分号后面只有空白、换行或文件结束。但是迁移文件中存在分号后接注释的写法:

ALTER TABLE injury_cases
ADD COLUMN injury_cause VARCHAR(50) NULL; -- 受伤原因说明

分号后存在注释,规则没有识别这个分隔位置。本次已经复现:007 文件中的多条 ALTER 和后面的 CREATE TABLE,被合并到同一个执行片段里。

影响: 当前 mysql2 连接没有启用多语句执行,多个 SQL 被当成一个语句发送,会导致迁移失败。即使第 1 条修复,仍可能在第 7 个迁移文件处中断。

处理方式: 最小调整是将行内注释移到独立行,并为现有迁移文件补拆分测试;长期可以采用可靠的迁移工具或 SQL 解析方案。

不能直接改成 split(';') 就认为可靠,因为 SQL 字符串本身可能包含分号;也不建议为了绕过问题全局打开多语句执行。

验收: 逐个检查迁移产生的语句;干净 MySQL 库运行全部迁移;第二次启动跳过已完成迁移;中途失败后有明确的恢复方法。

3. 收藏常用队员使用了 SQLite 专用 SQL

源码位置:server/services/DashboardService.ts 的 addFavorite。

当前代码使用:

INSERT OR IGNORE INTO favorite_persons ...

这是 SQLite 的写法,对应 MySQL 写法是:

INSERT IGNORE INTO favorite_persons ...

触发过程: 本地 SQLite 收藏正常;换成 MySQL 后系统可能能够启动、登录、查询人员,但点击“收藏常用队员”时才出现 SQL 错误。

只检查首页和登录发现不了这个问题。

处理方式: 根据数据库驱动选择对应语句,或者把差异封装到数据库适配层。同时检查其余业务 SQL,不能只修这一处就认定所有功能兼容。

验收: 首次收藏成功;重复收藏不产生重复记录;取消收藏成功;MySQL 和 SQLite 两种目标路径分别验证。

4. 编译后端时没有打包数据库迁移文件

源码位置:tsconfig.server.jsonserver/database/index.ts

执行 npm run build:server 会将 TypeScript 编译到 dist-server,但 tsc 不会自动复制 SQL 文件。

源码运行查找:server/database/migrations/mysql/
编译运行查找:dist-server/database/migrations/mysql/

当前逻辑发现迁移目录不存在后会直接跳过,随后初始化管理员时访问 users 表。

影响:

处理方式: 将资源复制纳入构建流程。例如,在后端编译成功后执行:

cp -R server/database/migrations dist-server/database/

同时检查迁移文件存在、数量与发布版本一致、关键迁移不缺失;缺失时让构建或启动明确报错。

验收: 用真正的发布包,在没有 server 源目录可供兜底的环境中启动,确认能够从编译产物读取并执行全部迁移。

这一项主要属于发布流程问题,不需要修改业务数据。

5. MySQL 开启事务的连接和实际查询连接不一致

源码位置:server/database/adapters/mysql.ts

当前事务逻辑大致如下:

const conn = await pool.getConnection();
await conn.beginTransaction();
await fn();
await conn.commit();

但是回调中的 run、all、exec 实际调用的是 pool.query,而不是上述 conn。

连接 A:开始事务
连接 B/C:执行业务 SQL
连接 A:提交或回滚

连接 A 的回滚,无法撤销连接 B/C 已经提交的写入。

业务例子: 一个操作先创建病例,再创建诊疗记录,最后写入状态日志。如果第二步失败,程序虽然执行 rollback,第一步仍可能留在数据库里,产生不完整业务数据。

处理方式: 保证事务范围内所有查询使用同一个连接,包括跨 Repository 调用。可显式传递事务连接,也可建立可靠的事务上下文;不能只修改 beginTransaction 的位置。

验收:

  1. 第一条写入成功后,主动让第二条写入失败。
  2. 验证第一条写入已经撤销。
  3. 验证正常路径能够提交。
  4. 验证并发请求的事务不会互相串用连接。

业务事务修复后,也不能认为所有 MySQL 建表、改表操作都能回滚。结构迁移的隐式提交与失败恢复需要单独处理。

6. ID 只在内存计数,同一天重启后重复

源码位置:server/utils/ids.ts

当前 ID 生成方式是“前缀 + 当天日期 + 内存递增序号”,计数器存在进程中的普通对象:

const counters: Record<string, number> = {};

服务重启后,该对象重新变空,序号重新从 1 开始。本次用两个独立进程复现了:

第一次启动:P202609080001
第二次启动:P202609080001

影响:

源码虽然有 seedCounter,但目前没有发现调用它恢复计数的代码。

处理方式: 使用数据库中持久化、原子递增的序号,或选择符合字段和业务规则的唯一 ID 方案;必要时分开内部主键与用户可见的业务编号。已有 ID 和关联必须保留,不能简单重编号。

只在启动时读取最大值,仍不能完全解决多进程并发分配的问题。

验收: 同日重启后新增成功;迁移旧数据后新增成功;并发新增无冲突;历史记录未被覆盖;病例展示编号的唯一性也需要核查。

7. 附件 ID 冲突可能先覆盖旧文件,再提示上传失败

源码位置:server/services/InjuryService.ts

当前附件上传顺序:

生成附件 ID
→ 根据 ID 计算磁盘路径
→ 写入文件
→ 向数据库插入附件记录

文件写入使用 fs.writeFile,默认允许覆盖已有文件。

可能的失败过程:

  1. 服务重启后,新附件获得旧附件的 ID。
  2. 根据 ID 得到相同文件路径。
  3. 新文件先覆盖旧文件。
  4. 数据库插入时才因主键重复失败。

最终可能出现“上传提示失败,但旧附件内容已经被替换”。这是基于代码顺序推导的风险,本次没有对真实附件执行破坏性复现。

处理方式:

数据库事务不能回滚磁盘文件,所以不能只修第 5 条。

验收: 比较旧附件在重启、模拟 ID 冲突、数据库写入失败前后的 SHA-256;旧文件必须不变,且不能留下无法关联的新增文件。

8. DB_DRIVER=mysql 不会把 SQLite 数据搬到 MySQL

源码位置:server/database/factory.ts

DB_DRIVER=sqlite → 连接 SQLite 文件
DB_DRIVER=mysql  → 连接指定 MySQL 数据库

配置只选择数据库连接方式,没有执行“从 SQLite 读取,再导入 MySQL”的操作。

直接切换的表现: MySQL 建表成功,创建了默认管理员,但原组织、人员、病例和诊疗记录不见了。原数据通常还在 SQLite 文件中,只是新系统不再读取它。

数据类别 迁移要求
角色、权限、账号 保留关联和 bcrypt 密码哈希,不能用默认账号覆盖原账号
组织、人员 保留主键、组织父子关系、人员归属
病例、诊疗、评定、草稿 保留关联,转换日期、空值和字段类型
收藏、审计等 按源库清单核对,避免漏表
附件 元数据与 data/uploads 文件一起迁移
迁移记录 以 MySQL 实际执行结果为准,不照搬 SQLite 标记冒充完成

处理流程: 停写并备份 SQLite 与附件 → 修复并创建目标 MySQL 结构 → 在副本上演练专用数据转换 → 导入并处理初始种子冲突 → 同步附件 → 核对数据 → 切换入口。

当前仓库没有发现可直接使用的完整 SQLite → MySQL 迁移程序,需要补齐转换与校验流程。不能将 SQLite .dump 原样导入 MySQL,也不能使用 INSERT IGNORE 静默跳过所有冲突。

验收: 逐表数量、主键集合、关联关系一致;原账号能登录;历史病例、日期和附件正确;迁移后能够继续新增数据。新系统开始接收正式写入后,不能简单切回旧 SQLite,否则可能丢失切换后的新数据。

完整部署手册第 12 节提供了更详细的迁移顺序和回退要求:SQLite → MySQL 部署与迁移手册

9. 监听地址与默认凭据需要明确处理

9.1 日志中的 127.0.0.1 不代表实际只监听本机

源码位置:server/index.ts

当前使用 app.listen(PORT),没有传入 host;日志虽然显示 127.0.0.1,实际可能监听所有地址。

处理: 明确传入绑定地址。若采用 Nginx 同机代理,后端绑定 127.0.0.1;设置 HOST 环境变量但不修改读取它的代码不会生效。安全组也不放通后端端口。

验收: 使用 ss -lntp 查看真实监听,确认外部不能直接连接 3001。

9.2 默认管理员和 JWT 开发密钥不能直接用于正式环境

源码位置:server/database/index.tsserver/services/AuthService.ts

首次初始化会创建默认管理员 admin,初始口令 admin123;JWT_SECRET 未设置时会采用源码中的开发默认值。

处理: 仅在访问受限的环境完成首次初始化;开放前修改管理员密码、设置随机 JWT_SECRET。当前没有查到可直接使用的通用修改密码 API,不能假设页面上一定有改密入口。

修改密码不会自动撤销既有 JWT;如果默认口令曾经暴露,需要同时轮换 JWT 密钥并重启。不要删除或改名唯一 admin 后直接重启,当前初始化逻辑可能重新创建默认 admin。

验收: 新密码登录成功、默认密码失败;环境变量确实注入服务;轮换密钥后旧令牌失效;未授权接口访问被拒绝。

建议修复与验收顺序

  1. 先解决初始化。 修建表 SQL、迁移拆分和迁移文件打包,在空 MySQL 上验证稳定启动。
  2. 再保证业务正确性。 修收藏方言、事务连接、ID 分配和附件保护,覆盖正常与失败路径。
  3. 完成部署配置。 明确监听范围、凭据、环境变量、服务托管和备份。
  4. 迁移真实数据。 先演练,再停写迁移,核对账号、关系、日期与附件。
  5. 完成整体验收。 登录、人员、病例、诊疗、草稿、收藏、附件、权限、重启及恢复都通过,才认定正式上线完成。

服务器环境可以先准备,但“服务已启动”和“首页返回 200”不能替代上述业务验收。切换到 CentOS 也不会消除这些代码与数据问题。