本项目采用**双数据源架构**,实现 SQL Server (OA系统) 与 MySQL (业务系统) 之间的数据同步。
┌─────────────────────────────────────────────────────────────┐
│ 应用层 │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ DepartmentSyncTask │ │ UserSyncTask │ │
│ │ (定时任务) │ │ (定时任务) │ │
│ └──────────┬───────────┘ └──────────┬─────────────┘ │
│ │ │ │
│ v v │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ DepartmentSyncController / UserSyncController │ │
│ │ (同步控制器 - 协调数据同步) │ │
│ └──────────┬──────────────────────────┬────────────────┘ │
└─────────────┼──────────────────────────┼────────────────────┘
│ │
v v
┌─────────────────────────────────────────────────────────────┐
│ 服务层 │
│ ┌─────────────────────────┐ ┌──────────────────────┐ │
│ │ DepartmentSyncServiceImpl│ │ UserSyncServiceImpl │ │
│ │ (同步业务逻辑) │ │ (同步业务逻辑) │ │
│ └────┬────────────────┬────┘ └────┬────────────┬────┘ │
│ │ │ │ │ │
│ │ SQL Server │ MySQL │ SQL Server │ MySQL │
│ │ 读取 │ 写入 │ 读取 │ 写入 │
└───────┼────────────────┼──────────────┼────────────┼────────┘
│ │ │ │
v v v v
┌─────────────────────────────────────────────────────────────┐
│ 数据访问层 │
│ ┌──────────────────┐ ┌─────────────┐ ┌────────────────┐ │
│ │ DepartmentSync │ │ SysDept │ │ UserSync │ │
│ │ Mapper │ │ Mapper │ │ Mapper │ │
│ │ @DataSource │ │ (默认源) │ │ @DataSource │ │
│ │ (SQLSERVER) │ │ │ │ (SQLSERVER) │ │
│ └────────┬─────────┘ └──────┬──────┘ └────────┬─────────┘ │
└───────────┼────────────────────┼──────────────────┼──────────┘
│ │ │
v v v
┌─────────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ SQL Server 数据库 │ │ MySQL 数据库 │ │ SQL Server 数据库│
│ ┌────────────────┐ │ │ ┌──────────┐ │ │ ┌──────────────┐ │
│ │ uv_department │ │ │ │ sys_dept │ │ │ │ OA_User │ │
│ │ (视图) │ │ │ └──────────┘ │ │ └──────────────┘ │
│ └────────────────┘ │ └──────────────┘ └──────────────────┘
└─────────────────────┘
文件路径: ruoyi-admin/src/main/java/com/ruoyi/web/controller/sqlserver/SqlServerDepartmentController.java
职责:
- 专门用于从 SQL Server 查询部门数据
- 不涉及任何 MySQL 操作
- 提供独立的查询接口
接口:java GET /sqlserver/department/branch/list 权限: @PreAuthorize("@ss.hasPermi('sqlserver:department:list')")
关键特性:
```java
@RestController
@RequestMapping("/sqlserver/department")
public class SqlServerDepartmentController {
@Autowired
private DepartmentSyncMapper departmentSyncMapper; // 带有 @DataSource 注解的 Mapper
@GetMapping("/branch/list")
public AjaxResult getBranchDepartments() {
// 此处会自动切换到 SQL Server 数据源
List<DepartmentSyncDTO> list = departmentSyncMapper.selectBranchDepartments();
return AjaxResult.success("查询成功", list);
}
}
```
文件路径: ruoyi-admin/src/main/java/com/ruoyi/web/controller/sqlserver/SqlServerUserController.java
职责:
- 专门用于从 SQL Server 查询用户数据
- 不涉及任何 MySQL 操作
- 提供独立的查询接口
接口:java GET /sqlserver/user/list 权限: @PreAuthorize("@ss.hasPermi('sqlserver:user:list')")
文件路径: ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/DepartmentSyncController.java
职责:
- 协调部门和用户的同步流程
- 调用 Service 层执行 SQL Server → MySQL 的数据同步
接口:java POST /system/dept/sync/branch // 同步部门 POST /system/dept/sync/user // 同步用户
文件路径: ruoyi-system/src/main/java/com/ruoyi/system/mapper/DepartmentSyncMapper.java
关键特性:java @DataSource(DataSourceType.SQLSERVER) // ⭐ 关键注解:标记使用 SQL Server 数据源 public interface DepartmentSyncMapper { List<DepartmentSyncDTO> selectBranchDepartments(); }
XML 映射:xml <mapper namespace="com.ruoyi.system.mapper.DepartmentSyncMapper"> <select id="selectBranchDepartments" resultMap="DepartmentSyncResult"> <![CDATA[ SELECT TOP 500 b.departmentID, b.departmentName, b.parentID, a.departmentName AS parentName FROM uv_department a WITH (NOLOCK) INNER JOIN uv_department b WITH (NOLOCK) ON a.departmentID = b.parentID WHERE a.departmentName = N'合作单位' ORDER BY b.departmentName ]]> </select> </mapper>
文件路径: ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserSyncMapper.java
关键特性:java @DataSource(DataSourceType.SQLSERVER) // ⭐ 关键注解:标记使用 SQL Server 数据源 public interface UserSyncMapper { List<UserSyncDTO> selectOaUsers(); }
数据源: 默认 MySQL(无需 @DataSource 注解)
职责:
- 在 MySQL 数据库中进行 CRUD 操作
- 写入从 SQL Server 同步过来的数据
文件路径: ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DepartmentSyncServiceImpl.java
数据流转过程:
```java
@Transactional
public AjaxResult syncBranchDepartments() {
// ========== 第一步:从 SQL Server 读取数据 ==========
// departmentSyncMapper 上有 @DataSource 注解,自动切换到 SQL Server
log.info("开始从 SQL Server 查询分公司数据...");
List branchDepts = departmentSyncMapper.selectBranchDepartments();
// ========== 第二步:写入 MySQL 数据库 ==========
// sysDeptMapper 使用默认数据源(MySQL)
log.info("开始将数据写入 MySQL 数据库...");
for (DepartmentSyncDTO dto : branchDepts) {
// 所有 sysDeptMapper 的调用都在 MySQL 中执行
SysDept existingBranch = sysDeptMapper.checkDeptNameUnique(branchName, 100L);
sysDeptMapper.insertDept(newBranch);
// ...
}
}
```
文件路径: ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserSyncServiceImpl.java
数据流转过程:
```java
@Transactional
public AjaxResult syncOaUsers() {
// ========== 第一步:从 SQL Server 读取用户数据 ==========
// userSyncMapper 上有 @DataSource 注解,自动切换到 SQL Server
log.info("开始从 SQL Server 查询 OA 用户数据...");
List oaUsers = userSyncMapper.selectOaUsers();
// ========== 第二步/第三步:查询 MySQL 部门并写入用户数据 ==========
// sysDeptMapper 和 sysUserMapper 都使用默认数据源(MySQL)
log.info("开始将用户数据写入 MySQL 数据库...");
for (UserSyncDTO dto : oaUsers) {
// 从 MySQL 查询部门信息
SysDept dept = sysDeptMapper.selectDeptByDepartmentId(dto.getDepartmentId());
// 在 MySQL 中创建或更新用户
SysUser existingUser = sysUserMapper.selectUserByOaUserId(dto.getOaUserId());
sysUserMapper.insertUser(newUser);
// ...
}
}
```
// 1. 在 Mapper 接口上添加注解
@DataSource(DataSourceType.SQLSERVER)
public interface DepartmentSyncMapper {
List<DepartmentSyncDTO> selectBranchDepartments();
}
// 2. 框架自动切换数据源
// 当调用 departmentSyncMapper.selectBranchDepartments() 时:
// - AOP 拦截器识别 @DataSource 注解
// - 动态切换到 SQL Server 数据源
// - 执行 SQL 查询
// - 查询完成后恢复默认数据源
配置文件: application-dev.yml
spring:
datasource:
# 主数据源 (MySQL)
druid:
master:
url: jdbc:mysql://localhost:3306/ry-vue?...
username: root
password: password
# 从数据源 (SQL Server)
slave:
enabled: true
url: jdbc:sqlserver://192.168.1.100:1433;DatabaseName=OA_DB
username: sa
password: password
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
| SQL Server (uv_department) | MySQL (sys_dept) | 说明 |
|---|---|---|
| departmentID | department_id | 外部系统部门ID (新增字段) |
| departmentName | dept_name | 部门名称 |
| parentID | parent_id | 父部门ID |
| SQL Server (OA_User) | MySQL (sys_user) | 说明 |
|---|---|---|
| OA_User_ID | oa_user_id | OA用户ID (新增字段) |
| OA_User | user_name | 用户名 |
| OA_Name | nick_name | 昵称 |
| OA_departmentID | department_id | 关联部门 (通过查询转换为 dept_id) |
| OA_gender | sex | 性别 |
| OA_email | 邮箱 | |
| OA_mobile | phonenumber | 手机号 |
用户/定时任务
│
v
DepartmentSyncController
│
└─> POST /system/dept/sync/branch
│
v
DepartmentSyncServiceImpl.syncBranchDepartments()
│
├─> Step 1: departmentSyncMapper.selectBranchDepartments()
│ │
│ └──> 切换到 SQL Server
│ │
│ v
│ SELECT FROM uv_department
│ │
│ v
│ 返回 List<DepartmentSyncDTO>
│
├─> Step 2: 解析部门名称 (湛江--护士)
│ │
│ └──> 分公司: 湛江分公司
│ └──> 部门: 护士
│
└─> Step 3: sysDeptMapper 操作 (默认 MySQL)
│
├─> 检查分公司是否存在
├─> 创建/更新分公司
├─> 检查部门是否存在
└─> 创建/更新部门
│
v
MySQL sys_dept 表
用户/定时任务
│
v
DepartmentSyncController
│
└─> POST /system/dept/sync/user
│
v
UserSyncServiceImpl.syncOaUsers()
│
├─> Step 1: userSyncMapper.selectOaUsers()
│ │
│ └──> 切换到 SQL Server
│ │
│ v
│ SELECT FROM OA_User
│ │
│ v
│ 返回 List<UserSyncDTO>
│
├─> Step 2: sysDeptMapper.selectDeptByDepartmentId()
│ │ (默认 MySQL)
│ v
│ 查找对应的 dept_id
│
└─> Step 3: sysUserMapper 操作 (默认 MySQL)
│
├─> 检查用户是否存在 (oa_user_id)
├─> 检查用户名是否已占用
└─> 创建/更新用户
│
v
MySQL sys_user 表
@Transactional // 事务管理
public AjaxResult syncBranchDepartments() {
// 1. 从 SQL Server 读取 (只读操作,不在事务内)
List<DepartmentSyncDTO> data = departmentSyncMapper.selectBranchDepartments();
// 2. 写入 MySQL (在事务内)
// 所有 MySQL 操作要么全部成功,要么全部回滚
sysDeptMapper.insertDept(dept1);
sysDeptMapper.insertDept(dept2);
// ...
}
注意事项:
- 事务仅对 默认数据源 (MySQL) 有效
- SQL Server 的查询操作 不在事务控制范围内(只读操作)
- 如果 MySQL 操作失败,会触发回滚,不影响 SQL Server 数据
// ✅ 好的做法:专门的同步 Controller
@RestController
@RequestMapping("/system/dept/sync")
public class DepartmentSyncController {
// 负责协调同步流程
}
```
在 Mapper 接口上明确标注数据源
java // ✅ 好的做法:Mapper 接口级别的注解 @DataSource(DataSourceType.SQLSERVER) public interface DepartmentSyncMapper { List<DepartmentSyncDTO> selectBranchDepartments(); }
在 Service 中添加清晰的注释
```java
// ✅ 好的做法:注释说明数据源切换点
// ========== 第一步:从 SQL Server 读取数据 ==========
// departmentSyncMapper 会自动切换到 SQL Server
List data = departmentSyncMapper.selectBranchDepartments();
// ========== 第二步:写入 MySQL 数据库 ==========
// sysDeptMapper 使用默认数据源
sysDeptMapper.insertDept(dept);
```
xml <!-- ✅ 好的做法:使用 CDATA 避免 XML 特殊字符问题 --> <select id="selectOaUsers" resultMap="UserSyncResult"> <![CDATA[ SELECT TOP 5000 * FROM OA_User WHERE LEN(RTRIM(LTRIM(OA_User))) > 0 ]]> </select> 不要在 Service 方法上添加 @DataSource 注解
java // ❌ 错误做法 @DataSource(DataSourceType.SQLSERVER) public AjaxResult syncBranchDepartments() { // 这会导致整个方法都使用 SQL Server 数据源 // 无法写入 MySQL! }
不要混用数据源而不添加注解
java // ❌ 错误做法:没有 @DataSource 注解 public interface DepartmentSyncMapper { // 会使用默认数据源 (MySQL),但实际需要查询 SQL Server List<DepartmentSyncDTO> selectBranchDepartments(); }
不要在 XML 中直接使用特殊字符
xml <!-- ❌ 错误做法 --> <select id="selectOaUsers"> SELECT * FROM OA_User WHERE count > 0 <!-- 会导致 XML 解析错误 --> </select>
# 查询部门数据
GET http://localhost:8080/sqlserver/department/branch/list
# 查询用户数据
GET http://localhost:8080/sqlserver/user/list
# 同步部门数据
POST http://localhost:8080/system/dept/sync/branch
# 同步用户数据
POST http://localhost:8080/system/dept/sync/user
在系统管理 > 定时任务中:
- OA数据同步: 调用 oaSyncTask.syncOaData(推荐,确保顺序)
- OA部门同步: 调用 departmentSyncTask.syncDepartments
- OA用户同步: 调用 userSyncTask.syncUsers
症状: 查询时报错 "Invalid object name 'uv_department'"
原因: @DataSource 注解未生效,使用了 MySQL 数据源
解决:
1. 检查 Mapper 接口是否添加了 @DataSource(DataSourceType.SQLSERVER) 注解
2. 确认 application-dev.yml 中 SQL Server 数据源配置正确
3. 检查 AOP 切面是否正常工作
症状: 在同一个 Service 方法中,第一次调用 SQL Server Mapper 成功,但第二次调用时使用了 MySQL 数据源
原因: 原始的 DataSourceAspect 实现在每次 Mapper 调用完成后都会清除数据源,导致后续调用回到默认数据源
修复方案:
已修改 DataSourceAspect.java 中的 around() 方法,实现智能数据源切换:
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 1. 记录当前数据源
String oldDataSourceType = DynamicDataSourceContextHolder.getDataSourceType();
boolean isNewDataSource = false;
// 2. 条件性切换数据源
if (StringUtils.isNotNull(dataSource)) {
String newDataSourceType = dataSource.value().name();
if (!newDataSourceType.equals(oldDataSourceType)) {
DynamicDataSourceContextHolder.setDataSourceType(newDataSourceType);
isNewDataSource = true;
}
}
try {
return point.proceed();
}
finally {
// 3. 智能恢复数据源(只有当本次调用改变了数据源时)
if (isNewDataSource) {
if (StringUtils.isNotEmpty(oldDataSourceType)) {
DynamicDataSourceContextHolder.setDataSourceType(oldDataSourceType);
} else {
DynamicDataSourceContextHolder.clearDataSourceType();
}
}
}
}
修复效果:
- ✅ 支持在同一 Service 中多次切换数据源
- ✅ 支持嵌套调用
- ✅ 支持 Service 间调用
- ✅ 完全兼容原有功能
详细说明: 请参阅 多数据源切换问题修复说明.md
症状: "The content of elements must consist of well-formed character data"
原因: SQL 语句中包含 <、>、& 等 XML 特殊字符
解决: 使用 <![CDATA[...]]> 包裹 SQL 语句
症状: SQL Server 数据已查询,但 MySQL 数据未写入
原因: MySQL 操作异常触发事务回滚
解决: 检查日志中的错误信息,修复 MySQL 操作中的问题
| 日期 | 版本 | 更新内容 | 更新人 |
|---|---|---|---|
| 2025-10-18 | 1.0 | 创建多数据源架构说明 | System |