企业考试系统从.NET迁移到Java+Linux:数据库迁移、接口兼容与灰度切换实践

一套企业考试系统运行五年、八年甚至更长时间以后,真正困难的往往已经不是"增加一个功能"。

人员数据可能积累到几万人,题库有几万甚至几十万道试题,历史考试已经进行了几百场,数据库里还保存着答卷、成绩、培训记录、证书和操作日志。这个时候,如果因为服务器环境、技术架构、移动端适配、国产化部署或者后续功能扩展需要升级系统,项目面对的实际上已经不是一次普通的软件版本更新,而是一次完整的业务系统迁移

以宏远培训考试系统为例,早期版本采用.NET技术体系,随着产品持续升级,目前新版已经全部转向Java技术体系,并采用前后端分离架构,可以在Linux环境下部署。

从表面看,这似乎只是:

复制代码
.NET
  ↓
Java

Windows环境
  ↓
Linux环境

但真正实施时,需要解决的问题远比"把代码重新写一遍"复杂。

旧系统里的人员ID怎么办?历史成绩怎么迁?原来的题库和试卷还能不能继续使用?已经考完多年的试卷怎样保证迁移以后仍然能够还原?旧接口不能立即下线怎么办?新旧系统如何平滑切换?迁移过程中如果出现问题又如何退回?

因此,企业考试系统从.NET迁移到Java+Linux,本质上不是一次语言替换,而是:

数据、业务规则、接口、运行环境和历史资产的一次整体重构。

本文不讨论".NET和Java谁更好",而是结合企业考试系统实际业务,以及宏远培训考试系统从.NET版本升级到Java前后端分离版本的过程思路,分析数据库迁移、接口兼容和灰度切换中真正需要考虑的问题。


一、迁移的第一原则:不要把项目理解成"代码翻译"

很多系统升级项目一开始容易陷入一个误区:

原来是.NET写的,现在用Java重新写一遍就行。

实际上,如果只是把:

复制代码
.NET Controller
.NET Service
.NET DAL

机械地翻译成:

复制代码
Java Controller
Java Service
Java Mapper

最终得到的可能只是一个"Java版旧系统"。

真正值得迁移的原因,通常并不是编程语言本身,而是原有架构已经难以适应新的业务要求。

例如旧系统可能形成:

复制代码
浏览器
   ↓
.NET Web应用
   ↓
业务逻辑
   ↓
数据库

页面、业务逻辑、数据访问之间耦合程度比较高。

而新版系统更适合采用:

复制代码
PC端 / 移动端
       ↓
    API接口
       ↓
Java后端业务服务
       ↓
Redis / 数据库 / 文件服务

前端负责交互展示,后端负责身份、权限、课程、题库、考试、成绩等业务规则。

这样一来,同一个Java后端服务可以为PC端、移动端以及后续第三方系统接口提供统一能力。

所以迁移的真正目标应该是:

保留有价值的数据和业务规则,重构已经不适合继续扩展的技术实现。


二、开始写新系统之前,先盘点旧系统到底有什么

一个已经运行多年的考试系统,真正有价值的资产并不是源代码,而是多年积累下来的业务数据。

通常至少包括:

数据类型 典型内容
组织数据 集团、单位、部门、班组
人员数据 姓名、工号、账号、岗位、组织关系
题库数据 题型、题干、选项、答案、知识点
试卷数据 固定卷、随机卷、试卷结构
考试数据 考试规则、考试对象、考试时间
答卷数据 考生实际试题、选择答案、主观题答案
成绩数据 总分、题目得分、排名
培训数据 课程、学习记录、学时
证书数据 证书编号、有效期
日志数据 登录、考试、监考、管理员操作

其中最危险的一个思想是:

反正新系统功能更多,旧数据直接导进去就可以。

数据库迁移不是简单执行:

复制代码
INSERT INTO new_table
SELECT * FROM old_table;

因为新版的数据模型很可能已经发生变化。

例如旧系统可能是:

复制代码
user
exam
score

而新版为了支持集团化权限、一人一档和考试过程还原,可能会进一步拆成:

复制代码
person
account
organization
exam
exam_session
exam_paper_snapshot
answer_record
score_record

所以迁移之前首先应该建立:

复制代码
旧数据模型
     ↓
字段映射
     ↓
业务转换规则
     ↓
新数据模型

而不是直接导数据库。


三、数据库迁移真正难的是"业务语义",不是字段类型

很多人第一次做数据库迁移,会重点检查:

复制代码
varchar → varchar

datetime → timestamp

int → bigint

这些当然需要处理,但这往往不是最难的部分。

真正容易出问题的是:

同一个字段,在新旧系统中的业务含义可能已经不同。

例如旧系统有:

复制代码
status = 1

到底是什么意思?

可能代表:

复制代码
启用

也可能代表:

复制代码
已交卷

甚至不同表里的:

复制代码
status = 1

意义完全不同。

因此迁移时最好建立明确的数据字典,例如:

旧字段 旧含义 新字段 转换规则
UserID 用户ID person_id 建立ID映射
UserName 登录账号 login_name 保留历史账号
DeptID 部门ID org_id 映射新组织树
ExamState 考试状态 status 枚举转换
Score 最终成绩 total_score 数值校验

可以专门建立一张:

复制代码
migration_mapping

记录:

复制代码
source_system
source_table
source_id
target_table
target_id
migration_batch
status

例如:

复制代码
OLD_EXAM
USER
5689
PERSON
782635921834
BATCH_20260901
SUCCESS

这样迁移以后仍然能够回答:

新系统PersonID 782635921834,在旧系统里到底是哪条用户数据?

这种可追溯能力在大规模迁移时非常重要。


四、人员不能按照登录名直接迁移

考试系统迁移中特别容易出问题的是人员数据。

假设旧系统运行八年,一个员工可能经历:

复制代码
手机号登录
   ↓
旧工号登录
   ↓
集团统一新工号

如果迁移程序直接:

复制代码
login_name不同
=
不同人员

一个员工很可能被迁移成三个人。

更加合理的方式应该是先建立人员主身份:

复制代码
旧账号
旧工号
新工号
外部HR编号
        ↓
     PersonID

以后考试、成绩、培训、证书真正关联的是:

复制代码
PersonID

而不是登录账号。

例如:

复制代码
OLD USER 1023
工号:A10086

OLD USER 5689
工号:JT20260856

          ↓

PersonID:782635921834

如果经过人员数据核验确认是同一个员工,就应该归并到同一个PersonID。

这也是新版宏远培训考试系统在人员档案管理中比较重要的一类设计思路:

账号可以调整,但人员历史档案不能因为账号变化而重新开始。

否则系统迁移虽然技术上成功了,企业最重要的"一人一档"却断掉了。


五、历史试卷不能只迁"当前题库"

考试系统的数据迁移还有一个与普通OA系统很不一样的地方:

考试结果必须能够解释。

假设张三2022年参加了一场考试:

复制代码
考试成绩:86分

到了2026年,管理员修改了题库中的第15题:

复制代码
原标准答案:B
新标准答案:C

如果历史试卷只是动态引用当前题库,那么重新打开2022年的答卷时就可能产生一个非常严重的问题:

当年的86分为什么现在算不出来了?

所以正式考试通常应该保存:

复制代码
Exam Paper Snapshot

也就是考试发生时的试卷快照。

例如:

复制代码
考试
  ↓
试卷实例
  ↓
题目快照
  ↓
考生答案
  ↓
当时标准答案
  ↓
题目得分

数据库迁移时也应该尽可能保留这种历史关系。

不能只迁:

复制代码
当前题库
+
最终总成绩

否则很多历史考试只能知道:

复制代码
张三86分

却无法回答:

复制代码
当时考了哪些题?
张三选择了什么?
每道题得了多少分?
为什么最后是86分?

对于需要长期留档的企业考试而言,这些数据往往比单独一个成绩字段更加重要。


六、密码迁移是最容易被忽略的一块

新旧系统认证体系发生变化以后,还有一个现实问题:

原来的密码怎么办?

正确做法绝对不是想办法"把用户密码解出来"。

正规的系统密码本身就不应该以可逆明文方式存储。

假设旧系统使用:

复制代码
Hash Algorithm A

新Java系统使用:

复制代码
Hash Algorithm B

可以采用一种渐进迁移方式。

用户第一次登录新系统时:

复制代码
输入密码
   ↓
按照旧算法验证
   ↓
验证成功
   ↓
按照新算法重新Hash
   ↓
保存新密码Hash

以后再次登录:

复制代码
直接使用新算法

可以抽象成:

复制代码
if (newPasswordVerifier.matches(rawPassword, passwordHash)) {

    loginSuccess();

} else if (legacyPasswordVerifier.matches(rawPassword, passwordHash)) {

    String newHash =
        newPasswordEncoder.encode(rawPassword);

    userRepository.updatePassword(newHash);

    loginSuccess();
}

如果旧系统的密码机制无法安全兼容,则宁可设计:

复制代码
首次登录重置密码

也不要为了"无感迁移"降低密码安全标准。


七、附件和文件路径同样属于迁移数据

考试系统的数据并不全部存在数据库。

题目中可能包含:

复制代码
图片
音频
视频
附件

课程中可能存在:

复制代码
PDF
Word
PPT
视频

旧系统可能保存:

复制代码
/upload/2020/question/001.jpg

新系统文件目录发生变化以后,如果只迁数据库:

复制代码
数据库迁移成功

但文件没有同步,新系统打开历史题目就可能全部显示:

复制代码
图片不存在

因此迁移对象应该至少包括:

复制代码
数据库
+
文件
+
附件关联关系

迁移完成以后可以通过程序进行文件完整性扫描:

复制代码
数据库记录附件数量
=
实际文件存在数量

并生成异常清单。


八、接口兼容的关键不是"一次性全部改掉"

大型企业中,考试系统通常不会孤立存在。

可能已经与:

复制代码
OA
HR
统一身份认证
门户
微信公众号
企业内部APP
第三方业务系统

建立接口。

如果新版Java系统上线当天直接宣布:

复制代码
所有旧接口全部停止

很容易出现新系统本身正常,其他系统全部报错的情况。

更加稳妥的做法可以增加:

复制代码
Compatibility Layer

也就是接口兼容层。

例如旧接口:

复制代码
/api/user/getUserInfo

新接口:

复制代码
/api/v2/person/profile

短期内可以保持:

复制代码
旧系统调用
      ↓
兼容适配器
      ↓
新版Java Service

适配器负责转换:

复制代码
旧请求格式
     ↓
新业务模型
     ↓
新返回结果
     ↓
转换成旧返回格式

这样就不需要要求所有第三方系统在同一天完成改造。


九、API最好从迁移阶段开始进行版本管理

接口升级过程中比较常见的一种设计是:

复制代码
/api/v1/
/api/v2/

例如:

复制代码
GET /api/v1/user/10086

继续服务旧系统。

新业务使用:

复制代码
GET /api/v2/person/10086

迁移期间:

复制代码
v1 → Compatibility Adapter → New Service
v2 → New Service

等第三方系统全部完成升级以后,再逐步下线:

复制代码
v1

这种方式比:

复制代码
一个接口不断修改参数和返回结构

更加容易管理。

因为后者很容易出现:

新系统好了,旧调用方坏了。


十、前后端分离以后,真正改变的是什么?

宏远培训考试系统新版切换到Java技术体系以后,一个比较明显的变化就是采用了前后端分离架构。

但"前后端分离"的意义绝不仅是:

复制代码
前端一个项目
后端一个项目

更重要的是形成了清晰的API边界。

例如:

复制代码
                  Java Backend
                       │
           ┌───────────┼───────────┐
           ▼           ▼           ▼
          PC端        移动端      第三方系统

人员、组织、题库、考试、成绩等核心业务规则集中在服务端。

不同终端主要负责不同的交互体验。

这对于培训考试系统尤其重要。

例如:

复制代码
正式考试
→ PC端

日常学习
→ PC / 移动端

管理后台
→ 浏览器

第三方平台
→ API

底层可以使用同一套人员、权限和考试业务数据。

相对于早期页面和后端业务逻辑耦合度较高的架构,这种方式更容易扩展新的访问端和系统接口。


十一、Java+Linux并不只是"换一个操作系统"

企业系统迁移到Java+Linux以后,真正的价值更多体现在部署体系的变化。

Java应用可以更加自然地运行在Linux服务器环境中。

对于越来越多采用:

复制代码
Linux服务器
私有云
内网环境
国产服务器环境

的政企用户而言,部署选择更加灵活。

同时,应用程序、数据库、文件和缓存等组件可以按照系统规模进行独立规划。

例如小规模场景可能是:

复制代码
Application
Database
Redis

部署在相对集中的环境。

而规模进一步扩大以后,则可以按照业务压力拆分:

复制代码
Gateway
   ↓
Application Nodes
   ↓
Redis
   ↓
Database

这里真正的优势不是"Java天然比.NET快",这种说法并不严谨。

更准确的表达应该是:

新的Java+Linux体系,为系统后续标准化部署、前后端解耦、接口开放以及多节点扩展提供了更加统一的技术基础。


十二、新旧系统切换最危险的方式:周五晚上直接关旧系统

系统迁移最容易出现问题的环节不是开发,而是:

Cutover。

一种很冒险的方式是:

复制代码
周五:
旧系统停止

周六:
全量迁移

周一:
所有用户直接进入新系统

一旦周一发现:

复制代码
历史成绩缺失
人员关系错误
附件打不开
第三方接口异常

项目就会非常被动。

更加稳妥的方式应该设计灰度阶段。


十三、第一阶段:迁移测试库,不影响正式系统

先从正式数据库复制一份:

复制代码
Production DB
      ↓
Migration Copy

然后完整执行一次迁移。

这一步重点不是让用户使用,而是发现:

复制代码
哪些字段映射失败
哪些人员无法匹配
哪些历史考试异常
哪些附件不存在

迁移程序本身也应该支持重复执行。

例如通过:

复制代码
source_system
+
source_record_id

建立唯一约束。

如果迁移中断,下一次执行可以继续,而不是再次生成重复数据。


十四、第二阶段:新系统进入业务验证

全量迁移测试完成以后,可以让部分管理员进入Java新版本进行验证。

重点检查:

复制代码
组织架构
人员数量
题库数量
历史考试
历史成绩
培训档案
证书
管理员权限

特别是要随机抽取一些已经离职、调岗、改工号的人员。

因为这些边界数据最容易暴露迁移问题。

例如抽查:

复制代码
正常在职员工
+
已经调岗员工
+
修改过工号员工
+
已经离职员工
+
重新入职员工

比只检查几个当前管理员账号有效得多。


十五、第三阶段:冻结旧系统写入,做最后一次增量迁移

如果第一次全量迁移是在9月1日,而正式切换是在9月10日,那么中间这9天旧系统仍然产生了:

复制代码
新人员
新题目
新考试
新成绩

所以正式切换之前需要一次:

复制代码
Delta Migration

即增量迁移。

典型过程可以是:

复制代码
旧系统正常运行
       ↓
完成历史全量迁移
       ↓
新系统验证
       ↓
确定切换时间
       ↓
旧系统进入只读
       ↓
执行最后一次增量迁移
       ↓
核对数据
       ↓
开放新系统

对于考试、成绩这类重要业务数据,我通常不建议为了追求"完全不停机"而轻易设计长时间双写。

因为:

复制代码
旧系统写一次
+
新系统写一次

看起来很高级,但会引入另一个更加复杂的问题:

两个系统写失败一个怎么办?

对正式考试数据而言,很多场景下一个经过安排的短时间只读窗口,加一次可验证的最终增量同步,反而比长期双写更加可控。


十六、灰度切换一定要预先设计回退方案

Cutover之前至少应该回答一个问题:

如果新系统正式开放以后两个小时发现严重问题,怎么办?

不能到时候再讨论。

例如可以保留:

复制代码
旧数据库备份
旧应用环境
最终迁移时间点
增量数据记录

同时规定:

复制代码
什么情况允许回退
谁决定回退
回退以后新产生的数据如何处理

系统迁移的专业程度,往往不是看:

"上线有没有出现问题。"

而是看:

出现问题以后有没有办法控制影响。


十七、迁移完成以后,不能只检查"总人数一样"

假设旧系统:

复制代码
人员:30000

新系统:

复制代码
人员:30000

并不能证明迁移正确。

因为完全可能:

复制代码
丢了10个人
+
重复了10个人

总数仍然是30000。

所以至少应该进行多维度核对。

例如:

检查项 核对内容
人员 数量、唯一人员、组织分布
题库 题目数量、题型数量、附件
考试 考试场次数
答卷 应有答卷数量
成绩 成绩数量、总分
证书 证书数量、有效期
档案 人员与历史记录关联
文件 文件数量、可访问状态

甚至可以对关键业务建立:

复制代码
Migration Verification Report

例如:

复制代码
旧系统考试记录:725
新系统考试记录:725

旧系统成绩记录:39311
新系统成绩记录:39311

异常映射:0
缺失附件:0
无法确认人员:3

最后3个人不要偷偷忽略。

应该进入:

复制代码
人工确认清单

处理完成以后再关闭迁移任务。


十八、迁移项目最值得保留的是"映射关系"

项目上线几个月以后,最容易被删除的一张表往往是:

复制代码
migration_mapping

但它其实非常有价值。

假设用户发现:

2021年的某场考试里少了一个人的成绩。

管理员可以通过:

复制代码
新PersonID
      ↓
Migration Mapping
      ↓
旧系统UserID
      ↓
旧成绩记录

快速定位。

如果没有映射,只剩两套完全不同的数据ID:

复制代码
旧系统:UserID=5689

新系统:PersonID=782635921834

排查起来会困难很多。

所以历史数据迁移完成以后,映射关系最好继续保留,至少作为迁移审计资料保存。


十九、以宏远培训考试系统为例,升级重点不是"把.NET换成Java"

宏远培训考试系统早期版本采用.NET技术体系。随着产品持续迭代,目前新版本已经全部升级为Java技术体系,并采用前后端分离架构,可面向Linux环境进行部署。

从产品长期演进角度看,这次变化真正重要的并不是:

复制代码
.NET → Java

这几个字本身。

更加重要的是借助版本升级重新整理:

复制代码
人员身份
组织权限
课程学习
题库试卷
考试过程
成绩档案
接口体系
部署环境

之间的关系。

例如在新版系统中,多层级组织、数据权限、移动端学习、PC端正式考试、题库管理、自动保存、考试日志、监考中心以及一人一档等业务,可以建立在更加清晰的前后端服务边界之上。

对于集团型企业而言,一套系统既可以服务单个单位,也可以按照集团---二级公司---三级单位---部门的组织层级实施数据权限管理。

对于需要系统集成的单位,则可以通过API、统一身份认证等方式进一步与已有业务系统连接。

这也是软件架构升级真正应该带来的价值:

不是重新做一遍原来的功能,而是让未来五年甚至更长时间的功能扩展不再被旧架构限制。


二十、旧版.NET数据是否还应该保留?

答案通常是:

应该。

即使Java新系统已经稳定运行,也不建议在刚刚切换以后立刻删除所有旧系统环境和原始数据库。

至少应该保留:

复制代码
原始数据库备份
迁移前完整备份
最终切换备份
Migration Mapping
迁移报告
异常处理记录

在确认所有历史数据、接口和业务流程稳定以后,再按照企业数据管理制度决定旧环境后续如何归档。

尤其对于运行多年、历史考试数据很多的企业而言:

新系统里的数据是一份迁移结果,原始数据库则是证明迁移结果的重要依据之一。


二十一、总结:系统迁移真正迁移的是"业务连续性"

企业考试系统从.NET升级到Java+Linux,如果只从开发语言来看,这只是一次技术栈变化。

但真正实施以后会发现,它涉及的是:

复制代码
代码架构
+
数据库模型
+
人员身份
+
历史档案
+
文件资源
+
第三方接口
+
部署环境
+
业务切换

其中任何一块没有处理好,都可能出现:

复制代码
系统能登录
但历史成绩没有了

系统能考试
但老接口不能用了

数据都导入了
但同一个员工变成两个人

总成绩存在
但历史答卷还原不了

所以一个相对稳妥的迁移过程应该是:

复制代码
旧系统盘点
      ↓
建立目标数据模型
      ↓
建立ID Mapping
      ↓
全量迁移
      ↓
数据校验
      ↓
接口兼容
      ↓
业务验证
      ↓
冻结旧系统写入
      ↓
增量迁移
      ↓
最终核对
      ↓
灰度切换
      ↓
稳定运行

以宏远培训考试系统从.NET版本升级到Java前后端分离版本为例,这次技术升级的意义也不应该被简单概括成"换了一种编程语言"。

真正重要的是:

旧数据能够继续使用,历史档案能够继续查询,原有业务能够平稳过渡,同时新架构又能够为后续Linux部署、移动端应用、系统集成、集团级权限、大规模考试以及持续功能升级留下空间。

对于已经运行五年、八年甚至十年的企业软件来说,最成功的升级往往不是用户发现:

"这个系统已经换成Java了。"

而是系统切换以后,用户打开自己的账号,曾经参加过的考试、培训、成绩和证书还在那里;管理员原来的业务仍然可以继续使用,而新的功能和新的技术架构已经在后台平稳接管。

从这个角度看,企业系统迁移最重要的技术指标其实不是:

迁移了多少张表,重写了多少行代码。

而是四个字:

业务不断。

相关推荐
香菜TTT1 小时前
Redis的哨兵机制
java·数据库·redis
withoutfear2 小时前
IntelliJ IDEA卡顿内存分配不足和索引被频繁触发问题解决办法
java·ide·intellij-idea
大圣编蚕2 小时前
Java StringWriter 详解:从入门到实战
java·开发语言·python
老苗项目实战3 小时前
Java工程师高频面试题(基于近一年的真实面试记录整理)
java·开发语言·面试·预见猿份·老苗项目实战
陈皮波比茶3 小时前
分布式任务调度xxl-job
java·分布式
XiaoYu1__3 小时前
JavaGUI文件管理系统开发实战:从静态展示到动态交互的演进
java·开发语言
霸道流氓气质3 小时前
MCP(Model Context Protocol)核心概念与Java SDK集成
java·开发语言
敲代码的嘎仔4 小时前
从集群锁失效到异步领券:我用分布式锁 + Redisson + MQ 把领券接口 RT 从 200ms 压到 15ms
java·数据库·redis·分布式·缓存·ai·wpf
步行cgn4 小时前
Spring 的模块体系详解
java·数据库·spring