江西医务康复系统: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.ts、server/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.json、server/database/index.ts。
执行 npm run build:server 会将 TypeScript 编译到 dist-server,但 tsc 不会自动复制 SQL 文件。
源码运行查找:server/database/migrations/mysql/
编译运行查找:dist-server/database/migrations/mysql/
当前逻辑发现迁移目录不存在后会直接跳过,随后初始化管理员时访问 users 表。
影响:
- 全新数据库: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 的位置。
验收:
- 第一条写入成功后,主动让第二条写入失败。
- 验证第一条写入已经撤销。
- 验证正常路径能够提交。
- 验证并发请求的事务不会互相串用连接。
业务事务修复后,也不能认为所有 MySQL 建表、改表操作都能回滚。结构迁移的隐式提交与失败恢复需要单独处理。
6. ID 只在内存计数,同一天重启后重复
源码位置:server/utils/ids.ts。
当前 ID 生成方式是“前缀 + 当天日期 + 内存递增序号”,计数器存在进程中的普通对象:
const counters: Record<string, number> = {};
服务重启后,该对象重新变空,序号重新从 1 开始。本次用两个独立进程复现了:
第一次启动:P202609080001
第二次启动:P202609080001
影响:
- 当天创建人员,重启后再创建人员,可能主键冲突。
- 组织、病例、诊疗、草稿、附件等使用该方法的记录存在同类风险。
- 多个后端进程同时运行,也可能各自生成相同 ID。
源码虽然有 seedCounter,但目前没有发现调用它恢复计数的代码。
处理方式: 使用数据库中持久化、原子递增的序号,或选择符合字段和业务规则的唯一 ID 方案;必要时分开内部主键与用户可见的业务编号。已有 ID 和关联必须保留,不能简单重编号。
只在启动时读取最大值,仍不能完全解决多进程并发分配的问题。
验收: 同日重启后新增成功;迁移旧数据后新增成功;并发新增无冲突;历史记录未被覆盖;病例展示编号的唯一性也需要核查。
7. 附件 ID 冲突可能先覆盖旧文件,再提示上传失败
源码位置:server/services/InjuryService.ts。
当前附件上传顺序:
生成附件 ID
→ 根据 ID 计算磁盘路径
→ 写入文件
→ 向数据库插入附件记录
文件写入使用 fs.writeFile,默认允许覆盖已有文件。
可能的失败过程:
- 服务重启后,新附件获得旧附件的 ID。
- 根据 ID 得到相同文件路径。
- 新文件先覆盖旧文件。
- 数据库插入时才因主键重复失败。
最终可能出现“上传提示失败,但旧附件内容已经被替换”。这是基于代码顺序推导的风险,本次没有对真实附件执行破坏性复现。
处理方式:
- 附件 ID 不重复。
- 文件使用独占创建等方式,已存在时拒绝覆盖。
- 数据库插入失败时,只清理本次新建的文件。
- 为数据库和磁盘之间的部分成功设计补偿机制。
数据库事务不能回滚磁盘文件,所以不能只修第 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.ts、server/services/AuthService.ts。
首次初始化会创建默认管理员 admin,初始口令 admin123;JWT_SECRET 未设置时会采用源码中的开发默认值。
处理: 仅在访问受限的环境完成首次初始化;开放前修改管理员密码、设置随机 JWT_SECRET。当前没有查到可直接使用的通用修改密码 API,不能假设页面上一定有改密入口。
修改密码不会自动撤销既有 JWT;如果默认口令曾经暴露,需要同时轮换 JWT 密钥并重启。不要删除或改名唯一 admin 后直接重启,当前初始化逻辑可能重新创建默认 admin。
验收: 新密码登录成功、默认密码失败;环境变量确实注入服务;轮换密钥后旧令牌失效;未授权接口访问被拒绝。
建议修复与验收顺序
- 先解决初始化。 修建表 SQL、迁移拆分和迁移文件打包,在空 MySQL 上验证稳定启动。
- 再保证业务正确性。 修收藏方言、事务连接、ID 分配和附件保护,覆盖正常与失败路径。
- 完成部署配置。 明确监听范围、凭据、环境变量、服务托管和备份。
- 迁移真实数据。 先演练,再停写迁移,核对账号、关系、日期与附件。
- 完成整体验收。 登录、人员、病例、诊疗、草稿、收藏、附件、权限、重启及恢复都通过,才认定正式上线完成。
服务器环境可以先准备,但“服务已启动”和“首页返回 200”不能替代上述业务验收。切换到 CentOS 也不会消除这些代码与数据问题。