# 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 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 selectBranchDepartments(); } ``` **XML 映射**: ```xml ``` #### 📍 UserSyncMapper **文件路径**: `ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserSyncMapper.java` **关键特性**: ```java @DataSource(DataSourceType.SQLSERVER) // ⭐ 关键注解:标记使用 SQL Server 数据源 public interface UserSyncMapper { List 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 注解工作原理 ```java // 1. 在 Mapper 接口上添加注解 @DataSource(DataSourceType.SQLSERVER) public interface DepartmentSyncMapper { List selectBranchDepartments(); } // 2. 框架自动切换数据源 // 当调用 departmentSyncMapper.selectBranchDepartments() 时: // - AOP 拦截器识别 @DataSource 注解 // - 动态切换到 SQL Server 数据源 // - 执行 SQL 查询 // - 查询完成后恢复默认数据源 ``` ### 数据源配置 **配置文件**: `application-dev.yml` ```yaml 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 │ ├─> 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 │ ├─> Step 2: sysDeptMapper.selectDeptByDepartmentId() │ │ (默认 MySQL) │ v │ 查找对应的 dept_id │ └─> Step 3: sysUserMapper 操作 (默认 MySQL) │ ├─> 检查用户是否存在 (oa_user_id) ├─> 检查用户名是否已占用 └─> 创建/更新用户 │ v MySQL sys_user 表 ``` --- ## 事务管理 ### 事务范围 ```java @Transactional // 事务管理 public AjaxResult syncBranchDepartments() { // 1. 从 SQL Server 读取 (只读操作,不在事务内) List 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 { // 负责协调同步流程 } ``` 2. **在 Mapper 接口上明确标注数据源** ```java // ✅ 好的做法:Mapper 接口级别的注解 @DataSource(DataSourceType.SQLSERVER) public interface DepartmentSyncMapper { List selectBranchDepartments(); } ``` 3. **在 Service 中添加清晰的注释** ```java // ✅ 好的做法:注释说明数据源切换点 // ========== 第一步:从 SQL Server 读取数据 ========== // departmentSyncMapper 会自动切换到 SQL Server List data = departmentSyncMapper.selectBranchDepartments(); // ========== 第二步:写入 MySQL 数据库 ========== // sysDeptMapper 使用默认数据源 sysDeptMapper.insertDept(dept); ``` 4. **使用 CDATA 包裹 SQL 语句** ```xml ``` ### ❌ 避免的做法 1. **不要在 Service 方法上添加 @DataSource 注解** ```java // ❌ 错误做法 @DataSource(DataSourceType.SQLSERVER) public AjaxResult syncBranchDepartments() { // 这会导致整个方法都使用 SQL Server 数据源 // 无法写入 MySQL! } ``` 2. **不要混用数据源而不添加注解** ```java // ❌ 错误做法:没有 @DataSource 注解 public interface DepartmentSyncMapper { // 会使用默认数据源 (MySQL),但实际需要查询 SQL Server List selectBranchDepartments(); } ``` 3. **不要在 XML 中直接使用特殊字符** ```xml ``` --- ## 接口使用指南 ### 1. 查询 SQL Server 数据 ```bash # 查询部门数据 GET http://localhost:8080/sqlserver/department/branch/list # 查询用户数据 GET http://localhost:8080/sqlserver/user/list ``` ### 2. 同步数据到 MySQL ```bash # 同步部门数据 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()` 方法,实现智能数据源切换: ```java @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](多数据源切换问题修复说明.md) **症状**: "The content of elements must consist of well-formed character data" **原因**: SQL 语句中包含 `<`、`>`、`&` 等 XML 特殊字符 **解决**: 使用 `` 包裹 SQL 语句 ### 问题4: 事务回滚导致数据不一致 **症状**: SQL Server 数据已查询,但 MySQL 数据未写入 **原因**: MySQL 操作异常触发事务回滚 **解决**: 检查日志中的错误信息,修复 MySQL 操作中的问题 --- ## 更新历史 | 日期 | 版本 | 更新内容 | 更新人 | |------|------|----------|--------| | 2025-10-18 | 1.0 | 创建多数据源架构说明 | System | --- ## 相关文档 - [多数据源切换问题修复说明.md](多数据源切换问题修复说明.md) - **重要!必读** - [SQL Server数据源查询优化说明.md](SQL Server数据源查询优化说明.md) - [部门同步功能开发总结.md](部门同步功能开发总结.md) - [用户同步功能开发总结.md](用户同步功能开发总结.md) - [OA数据同步定时任务使用指南.md](OA数据同步定时任务使用指南.md)