编辑 | blame | 历史 | 原始文档

OA 数据同步多数据源架构说明

架构概述

本项目采用**双数据源架构**,实现 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    │ │
│  │   (视图)       │ │  │ └──────────┘ │  │ └──────────────┘ │
│  └────────────────┘ │  └──────────────┘  └──────────────────┘
└─────────────────────┘

核心组件说明

1. 专门的 SQL Server 查询 Controller

📍 SqlServerDepartmentController

文件路径: 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);
}

}
```

📍 SqlServerUserController

文件路径: 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')")


2. 数据同步 Controller

📍 DepartmentSyncController

文件路径: 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 // 同步用户


3. 数据访问层 (Mapper)

📍 DepartmentSyncMapper

文件路径: 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>

📍 UserSyncMapper

文件路径: ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserSyncMapper.java

关键特性:
java @DataSource(DataSourceType.SQLSERVER) // ⭐ 关键注解:标记使用 SQL Server 数据源 public interface UserSyncMapper { List<UserSyncDTO> selectOaUsers(); }

📍 SysDeptMapper & SysUserMapper

数据源: 默认 MySQL(无需 @DataSource 注解)

职责:
- 在 MySQL 数据库中进行 CRUD 操作
- 写入从 SQL Server 同步过来的数据


4. 业务逻辑层 (Service)

📍 DepartmentSyncServiceImpl

文件路径: 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);
    // ...
}

}
```

📍 UserSyncServiceImpl

文件路径: 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);
    // ...
}

}
```


数据源切换机制

@DataSource 注解工作原理

// 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 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 数据


最佳实践

✅ 推荐做法

  1. 分离查询和同步职责
    ```java
    // ✅ 好的做法:专门的 SQL Server 查询 Controller
    @RestController
    @RequestMapping("/sqlserver/department")
    public class SqlServerDepartmentController {
    // 只负责查询 SQL Server 数据
    }

// ✅ 好的做法:专门的同步 Controller
@RestController
@RequestMapping("/system/dept/sync")
public class DepartmentSyncController {
// 负责协调同步流程
}
```

  1. 在 Mapper 接口上明确标注数据源
    java // ✅ 好的做法:Mapper 接口级别的注解 @DataSource(DataSourceType.SQLSERVER) public interface DepartmentSyncMapper { List<DepartmentSyncDTO> selectBranchDepartments(); }

  2. 在 Service 中添加清晰的注释
    ```java
    // ✅ 好的做法:注释说明数据源切换点
    // ========== 第一步:从 SQL Server 读取数据 ==========
    // departmentSyncMapper 会自动切换到 SQL Server
    List data = departmentSyncMapper.selectBranchDepartments();

// ========== 第二步:写入 MySQL 数据库 ==========
// sysDeptMapper 使用默认数据源
sysDeptMapper.insertDept(dept);
```

  1. 使用 CDATA 包裹 SQL 语句
    xml <!-- ✅ 好的做法:使用 CDATA 避免 XML 特殊字符问题 --> <select id="selectOaUsers" resultMap="UserSyncResult"> <![CDATA[ SELECT TOP 5000 * FROM OA_User WHERE LEN(RTRIM(LTRIM(OA_User))) > 0 ]]> </select>

❌ 避免的做法

  1. 不要在 Service 方法上添加 @DataSource 注解
    java // ❌ 错误做法 @DataSource(DataSourceType.SQLSERVER) public AjaxResult syncBranchDepartments() { // 这会导致整个方法都使用 SQL Server 数据源 // 无法写入 MySQL! }

  2. 不要混用数据源而不添加注解
    java // ❌ 错误做法:没有 @DataSource 注解 public interface DepartmentSyncMapper { // 会使用默认数据源 (MySQL),但实际需要查询 SQL Server List<DepartmentSyncDTO> selectBranchDepartments(); }

  3. 不要在 XML 中直接使用特殊字符
    xml <!-- ❌ 错误做法 --> <select id="selectOaUsers"> SELECT * FROM OA_User WHERE count > 0 <!-- 会导致 XML 解析错误 --> </select>


接口使用指南

1. 查询 SQL Server 数据

# 查询部门数据
GET http://localhost:8080/sqlserver/department/branch/list

# 查询用户数据
GET http://localhost:8080/sqlserver/user/list

2. 同步数据到 MySQL

# 同步部门数据
POST http://localhost:8080/system/dept/sync/branch

# 同步用户数据
POST http://localhost:8080/system/dept/sync/user

3. 定时任务

在系统管理 > 定时任务中:
- OA数据同步: 调用 oaSyncTask.syncOaData(推荐,确保顺序)
- OA部门同步: 调用 departmentSyncTask.syncDepartments
- OA用户同步: 调用 userSyncTask.syncUsers


故障排查

问题1: 数据源切换失败

症状: 查询时报错 "Invalid object name 'uv_department'"

原因: @DataSource 注解未生效,使用了 MySQL 数据源

解决:
1. 检查 Mapper 接口是否添加了 @DataSource(DataSourceType.SQLSERVER) 注解
2. 确认 application-dev.yml 中 SQL Server 数据源配置正确
3. 检查 AOP 切面是否正常工作

问题2: 同一 Service 中多次切换数据源失败 ⭐ 已修复

症状: 在同一个 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 语句

问题4: 事务回滚导致数据不一致

症状: SQL Server 数据已查询,但 MySQL 数据未写入

原因: MySQL 操作异常触发事务回滚

解决: 检查日志中的错误信息,修复 MySQL 操作中的问题


更新历史

日期 版本 更新内容 更新人
2025-10-18 1.0 创建多数据源架构说明 System

相关文档