diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysUserController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysUserController.java index 608ab2fc..fbe41f8c 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysUserController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/system/SysUserController.java @@ -29,6 +29,7 @@ import com.bms.common.enums.BusinessType; import com.bms.common.utils.SecurityUtils; import com.bms.common.utils.StringUtils; import com.bms.common.utils.poi.ExcelUtil; +import com.bms.system.orgsync.OrgSyncService; import com.bms.system.service.ISysDeptService; import com.bms.system.service.ISysPostService; import com.bms.system.service.ISysRoleService; @@ -55,6 +56,9 @@ public class SysUserController extends BaseController @Autowired private ISysPostService postService; + @Autowired + private OrgSyncService orgSyncService; + /** * 获取用户列表 */ @@ -67,6 +71,19 @@ public class SysUserController extends BaseController return getDataTable(list); } + /** + * 从老系统(Oracle)同步部门/角色/用户。 + * 覆盖式迁移:除内置超级管理员外,现有演示部门、角色、用户将被替换。 + */ + @PreAuthorize("@ss.hasPermi('system:user:add')") + @Log(title = "用户管理-老系统组织同步", businessType = BusinessType.IMPORT) + @PostMapping("/syncFromOracle") + public AjaxResult syncFromOracle() + { + Map result = orgSyncService.syncFromOracle(); + return AjaxResult.success(String.valueOf(result.get("summary")), result); + } + /** * 获取用户下拉选项(业务员/客服/采购员等选择用)。 * 不加 @PreAuthorize:登录即可访问,只返回 id/账号/姓名/部门等展示字段, diff --git a/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OracleOrgReader.java b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OracleOrgReader.java new file mode 100644 index 00000000..c1a7569b --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OracleOrgReader.java @@ -0,0 +1,66 @@ +package com.bms.system.orgsync; + +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.github.pagehelper.PageHelper; +import com.bms.common.annotation.DataSource; +import com.bms.common.enums.DataSourceType; +import com.bms.system.orgsync.mapper.OracleOrgSyncMapper; + +/** + * Oracle 组织/用户/角色读取器。 + * 走 SLAVE(Oracle 只读账号)数据源;必须独立 Bean, + * 否则 @DataSource 切面在同类自调用时不生效。 + * + * @author bms + */ +@Service +public class OracleOrgReader +{ + @Autowired + private OracleOrgSyncMapper oracleOrgSyncMapper; + + @DataSource(DataSourceType.SLAVE) + public List> readAllDept() + { + try + { + PageHelper.clearPage(); + return oracleOrgSyncMapper.selectAllTabDept(); + } + finally + { + PageHelper.clearPage(); + } + } + + @DataSource(DataSourceType.SLAVE) + public List> readAllEmployee() + { + try + { + PageHelper.clearPage(); + return oracleOrgSyncMapper.selectAllTabEmployee(); + } + finally + { + PageHelper.clearPage(); + } + } + + @DataSource(DataSourceType.SLAVE) + public List> readAllRole() + { + try + { + PageHelper.clearPage(); + return oracleOrgSyncMapper.selectAllSysRole(); + } + finally + { + PageHelper.clearPage(); + } + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OrgSyncService.java b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OrgSyncService.java new file mode 100644 index 00000000..dd5d84b8 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OrgSyncService.java @@ -0,0 +1,350 @@ +package com.bms.system.orgsync; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.bms.common.core.domain.entity.SysDept; +import com.bms.common.core.domain.entity.SysRole; +import com.bms.common.core.domain.entity.SysUser; + +/** + * 老系统组织架构(Oracle)-> BMS(MySQL)覆盖式迁移。 + * + * 部门:TAB_DEPT 11 个部门统一挂到根节点“威狮国际”(dept_id=200)下,子部门 201 起顺序编号; + * 角色:T_SYS_ROLE 有效内部角色原样迁入,role_id 沿用 Oracle ID,不带菜单权限; + * 用户:TAB_EMPLOYEE 全量迁入,EMPLOYEE_CODE 作登录账号;user_id 取 Oracle USER_ID, + * USER_ID 为空时取 EMP_ID;默认密码 123456;不分配角色。 + * 内置超级管理员 admin(user_id=1)/超级管理员角色(role_id=1)保留,仅将 admin 调到根部门。 + * + * @author bms + */ +@Service +public class OrgSyncService +{ + private static final Logger log = LoggerFactory.getLogger(OrgSyncService.class); + + /** 根部门 ID(威狮国际) */ + public static final long ROOT_DEPT_ID = 200L; + private static final long FIRST_CHILD_DEPT_ID = 201L; + private static final String ROOT_DEPT_NAME = "威狮国际"; + + /** 写库人标记 */ + public static final String SYNC_MARKER = "oracle-sync"; + + /** 新用户默认密码(BCrypt,明文 123456) */ + private static final String DEFAULT_PASSWORD = + "$2a$10$gAOzrNJaYLM2m85.9ykkLO0IPvQktxqee8RVpAu1e3.J0Ujiv4Z0G"; + + /** 老系统内置 admin 账号不迁移,避免覆盖 BMS 超级管理员 */ + private static final String BUILTIN_ADMIN = "admin"; + + @Autowired + private OracleOrgReader oracleOrgReader; + + @Autowired + private OrgSyncWriter orgSyncWriter; + + /** + * 执行一次全量迁移 + */ + public Map syncFromOracle() + { + long t0 = System.currentTimeMillis(); + + List> deptRows = oracleOrgReader.readAllDept(); + List> roleRows = oracleOrgReader.readAllRole(); + List> empRows = oracleOrgReader.readAllEmployee(); + + // ---- 部门 ---- + List depts = buildDepts(deptRows); + Map deptIdByCode = new HashMap<>(); + for (SysDept d : depts) + { + if (d.getParentId() == ROOT_DEPT_ID) + { + deptIdByCode.put(d.getDeptName(), d.getDeptId()); + } + } + + // ---- 角色 ---- + List roles = buildRoles(roleRows); + + // ---- 用户 ---- + List users = buildUsers(empRows, deptIdByCode); + + orgSyncWriter.writeAll(depts, roles, users, ROOT_DEPT_ID); + + long disabled = users.stream().filter(u -> "1".equals(u.getStatus())).count(); + long cost = System.currentTimeMillis() - t0; + String summary = String.format( + "老系统组织同步完成:部门 %d 个(含根节点“%s”),角色 %d 个(不含权限),用户 %d 个(启用 %d/停用 %d,默认密码 123456),耗时 %dms", + depts.size(), ROOT_DEPT_NAME, roles.size(), users.size(), users.size() - disabled, disabled, cost); + log.info(summary); + + Map result = new LinkedHashMap<>(); + result.put("deptCount", depts.size()); + result.put("roleCount", roles.size()); + result.put("userCount", users.size()); + result.put("enabledUserCount", users.size() - disabled); + result.put("disabledUserCount", disabled); + result.put("costMs", cost); + result.put("summary", summary); + return result; + } + + private List buildDepts(List> rows) + { + List list = new ArrayList<>(); + SysDept root = newDept(ROOT_DEPT_ID, 0L, "0", ROOT_DEPT_NAME, 0); + list.add(root); + long id = FIRST_CHILD_DEPT_ID; + int order = 1; + Set seen = new HashSet<>(); + for (Map row : rows) + { + String code = clean(str(row, "DEPT_CODE")); + String name = clean(str(row, "DEPT_NAME")); + String deptName = name != null ? name : code; + if (code == null || !seen.add(code)) + { + continue; + } + list.add(newDept(id++, ROOT_DEPT_ID, "0," + ROOT_DEPT_ID, deptName, order++)); + } + return list; + } + + private SysDept newDept(long id, long parentId, String ancestors, String name, int order) + { + SysDept d = new SysDept(); + d.setDeptId(id); + d.setParentId(parentId); + d.setAncestors(ancestors); + d.setDeptName(name); + d.setOrderNum(order); + d.setStatus("0"); + d.setDelFlag("0"); + d.setCreateBy(SYNC_MARKER); + d.setCreateTime(new Date()); + return d; + } + + private List buildRoles(List> rows) + { + List list = new ArrayList<>(); + Set seenIds = new HashSet<>(); + int sort = 1; + for (Map row : rows) + { + Long rid = lng(row, "ID"); + String name = clean(str(row, "ROLE_NAME")); + if (rid == null || name == null || !seenIds.add(rid)) + { + continue; + } + SysRole r = new SysRole(); + r.setRoleId(rid); + r.setRoleName(truncate(name, 30)); + r.setRoleKey("ORA_" + rid); + r.setRoleSort(sort++); + r.setDataScope("1"); + r.setMenuCheckStrictly(true); + r.setDeptCheckStrictly(true); + r.setStatus("0"); + r.setDelFlag("0"); + r.setCreateBy(SYNC_MARKER); + r.setCreateTime(new Date()); + r.setRemark(truncate(clean(str(row, "ROLE_DESC")), 500)); + list.add(r); + } + return list; + } + + private List buildUsers(List> rows, Map deptIdByCode) + { + List list = new ArrayList<>(); + Set seenIds = new HashSet<>(); + Set seenNames = new HashSet<>(); + int skipped = 0; + for (Map row : rows) + { + String code = clean(str(row, "EMPLOYEE_CODE")); + String name = clean(str(row, "EMPLOYEE_NAME")); + if (code == null || name == null) + { + skipped++; + continue; + } + if (BUILTIN_ADMIN.equalsIgnoreCase(code)) + { + // 保留 BMS 内置超级管理员,不被老系统 admin 覆盖 + skipped++; + continue; + } + // ID:优先 USER_ID,为空取 EMP_ID + Long uid = lng(row, "USER_ID"); + if (uid == null) + { + uid = lng(row, "EMP_ID"); + } + if (uid == null || !seenIds.add(uid) || !seenNames.add(code)) + { + log.warn("用户同步跳过:{}({}),ID 缺失或重复", code, name); + skipped++; + continue; + } + + SysUser u = new SysUser(); + u.setUserId(uid); + String deptCode = clean(str(row, "DEPT_CODE")); + Long deptId = deptCode == null ? null : deptIdByCode.get(deptCode); + u.setDeptId(deptId != null ? deptId : ROOT_DEPT_ID); + u.setUserName(code); + u.setNickName(truncate(name, 30)); + u.setPassword(DEFAULT_PASSWORD); + u.setStatus("1".equals(clean(str(row, "BL_OPEN"))) ? "0" : "1"); + u.setSex(mapSex(clean(str(row, "SEX")))); + u.setEmail(cleanEmail(str(row, "EMAIL"))); + u.setPhonenumber(cleanPhone(str(row, "MOBILE_PHONE"))); + u.setRemark(truncate(clean(str(row, "REMARK")), 500)); + Object createDate = row.get("CREATE_DATE"); + if (createDate instanceof Date) + { + u.setCreateTime((Date) createDate); + } + else + { + u.setCreateTime(new Date()); + } + u.setCreateBy(SYNC_MARKER); + list.add(u); + } + if (skipped > 0) + { + log.warn("用户同步共跳过 {} 条(内置 admin / 编码或ID重复 / 关键字段为空)", skipped); + } + return list; + } + + /** Oracle:1男 0女;BMS:0男 1女 2未知 */ + private static String mapSex(String sex) + { + if ("1".equals(sex)) + { + return "0"; + } + if ("0".equals(sex)) + { + return "1"; + } + return "2"; + } + + /** 老系统手机号存在加密脏值,仅接收纯号码/短横线/加号格式且不超过 11 位 */ + private static String cleanPhone(String v) + { + String s = clean(v); + if (s == null) + { + return null; + } + s = s.replace(" ", ""); + if (s.length() > 11 || !s.matches("[0-9+\\-]{6,11}")) + { + return null; + } + return s; + } + + private static String cleanEmail(String v) + { + String s = clean(v); + if (s == null || s.length() > 50 || !s.contains("@")) + { + return null; + } + return s; + } + + private static String truncate(String v, int max) + { + if (v == null) + { + return null; + } + return v.length() > max ? v.substring(0, max) : v; + } + + private static String clean(String v) + { + if (v == null) + { + return null; + } + String s = v.trim(); + return s.isEmpty() ? null : s; + } + + private static String str(Map row, String key) + { + Object v = getIgnoreCase(row, key); + return v == null ? null : String.valueOf(v); + } + + /** Oracle NUMBER 经 JDBC 多为 BigDecimal */ + private static Long lng(Map row, String key) + { + Object v = getIgnoreCase(row, key); + if (v == null) + { + return null; + } + if (v instanceof Number) + { + return ((Number) v).longValue(); + } + String s = String.valueOf(v).trim(); + if (s.isEmpty()) + { + return null; + } + int dot = s.indexOf('.'); + if (dot >= 0) + { + s = s.substring(0, dot); + } + try + { + return Long.parseLong(s); + } + catch (NumberFormatException e) + { + return null; + } + } + + private static Object getIgnoreCase(Map row, String key) + { + if (row.containsKey(key)) + { + return row.get(key); + } + for (Map.Entry e : row.entrySet()) + { + if (e.getKey() != null && e.getKey().equalsIgnoreCase(key)) + { + return e.getValue(); + } + } + return null; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OrgSyncWriter.java b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OrgSyncWriter.java new file mode 100644 index 00000000..f0fa4a97 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/OrgSyncWriter.java @@ -0,0 +1,62 @@ +package com.bms.system.orgsync; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.bms.common.core.domain.entity.SysDept; +import com.bms.common.core.domain.entity.SysRole; +import com.bms.common.core.domain.entity.SysUser; +import com.bms.system.orgsync.mapper.SysOrgSyncWriterMapper; + +/** + * 组织迁移写库。独立 Bean:保证 @Transactional 经代理生效,且与 SLAVE 读连接隔离。 + * 单个事务内覆盖式重建:除内置超级管理员(user_id=1 / role_id=1)外全部替换。 + * + * @author bms + */ +@Service +public class OrgSyncWriter +{ + /** 用户批量插入每批条数 */ + private static final int USER_BATCH = 200; + + @Autowired + private SysOrgSyncWriterMapper writerMapper; + + @Transactional(rollbackFor = Exception.class) + public void writeAll(List depts, List roles, List users, Long adminDeptId) + { + // 1. 清关联(保留 admin <-> 超级管理员角色) + writerMapper.deleteAllRoleDept(); + writerMapper.deleteRoleMenuKeepAdmin(); + writerMapper.deleteUserRoleKeepAdmin(); + writerMapper.deleteUserPostKeepAdmin(); + + // 2. 清用户/角色/部门 + writerMapper.deleteUsersKeepAdmin(); + writerMapper.deleteRolesKeepAdmin(); + writerMapper.deleteAllDept(); + + // 3. 重建部门 + for (SysDept d : depts) + { + writerMapper.insertDept(d); + } + + // 4. 重建角色(不带任何菜单权限) + for (SysRole r : roles) + { + writerMapper.insertRole(r); + } + + // 5. 重建用户(分批,避免单条 SQL 过长) + for (int i = 0; i < users.size(); i += USER_BATCH) + { + writerMapper.batchInsertUser(users.subList(i, Math.min(i + USER_BATCH, users.size()))); + } + + // 6. 超级管理员挂到根部门 + writerMapper.updateUserDept(1L, adminDeptId); + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/orgsync/mapper/OracleOrgSyncMapper.java b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/mapper/OracleOrgSyncMapper.java new file mode 100644 index 00000000..630d2184 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/mapper/OracleOrgSyncMapper.java @@ -0,0 +1,21 @@ +package com.bms.system.orgsync.mapper; + +import java.util.List; +import java.util.Map; + +/** + * 老系统 Oracle 组织/用户/角色只读查询(SLAVE 数据源,账号只读) + * + * @author bms + */ +public interface OracleOrgSyncMapper +{ + /** TAB_DEPT 全部部门(扁平结构,DEPT_CODE=DEPT_NAME) */ + List> selectAllTabDept(); + + /** TAB_EMPLOYEE 全部员工 */ + List> selectAllTabEmployee(); + + /** T_SYS_ROLE 有效角色(REC_FLAG=1,内部角色 ROLE_TYPE=100) */ + List> selectAllSysRole(); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/system/orgsync/mapper/SysOrgSyncWriterMapper.java b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/mapper/SysOrgSyncWriterMapper.java new file mode 100644 index 00000000..e8c74b8b --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/system/orgsync/mapper/SysOrgSyncWriterMapper.java @@ -0,0 +1,46 @@ +package com.bms.system.orgsync.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.bms.common.core.domain.entity.SysDept; +import com.bms.common.core.domain.entity.SysRole; +import com.bms.common.core.domain.entity.SysUser; + +/** + * 组织架构老系统数据迁移写库(MASTER 数据源)。 + * 覆盖式迁移:除内置超级管理员(user_id=1 / role_id=1)外, + * 现有演示部门、角色、用户全部清空后按 Oracle 数据重建。 + * + * @author bms + */ +public interface SysOrgSyncWriterMapper +{ + /** 清空角色-部门(自定义数据范围)关联 */ + int deleteAllRoleDept(); + + /** 清空全部部门 */ + int deleteAllDept(); + + /** 新增部门 */ + int insertDept(SysDept dept); + + /** 清空除超级管理员角色(role_id=1)以外角色的菜单、用户关联 */ + int deleteRoleMenuKeepAdmin(); + int deleteUserRoleKeepAdmin(); + + /** 删除除超级管理员角色外的全部角色 */ + int deleteRolesKeepAdmin(); + + /** 新增角色 */ + int insertRole(SysRole role); + + /** 删除除超级管理员(user_id=1)以外用户的岗位关联与用户 */ + int deleteUserPostKeepAdmin(); + int deleteUsersKeepAdmin(); + + /** 新增用户(批量) */ + int batchInsertUser(@Param("list") List list); + + /** 将指定用户挂到根部门 */ + int updateUserDept(@Param("userId") Long userId, @Param("deptId") Long deptId); +} diff --git a/bms-back/bms-system/src/main/resources/mapper/system/OracleOrgSyncMapper.xml b/bms-back/bms-system/src/main/resources/mapper/system/OracleOrgSyncMapper.xml new file mode 100644 index 00000000..63f8930c --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/system/OracleOrgSyncMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/bms-back/bms-system/src/main/resources/mapper/system/SysOrgSyncWriterMapper.xml b/bms-back/bms-system/src/main/resources/mapper/system/SysOrgSyncWriterMapper.xml new file mode 100644 index 00000000..79099ab0 --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/system/SysOrgSyncWriterMapper.xml @@ -0,0 +1,65 @@ + + + + + + delete from sys_role_dept + + + + delete from sys_dept + + + + insert into sys_dept(dept_id, parent_id, ancestors, dept_name, order_num, leader, phone, email, + status, del_flag, create_by, create_time) + values(#{deptId}, #{parentId}, #{ancestors}, #{deptName}, #{orderNum}, #{leader}, #{phone}, #{email}, + #{status}, '0', #{createBy}, sysdate()) + + + + delete from sys_role_menu where role_id <> 1 + + + + delete from sys_user_role where role_id <> 1 + + + + delete from sys_role where role_id <> 1 + + + + insert into sys_role(role_id, role_name, role_key, role_sort, data_scope, + menu_check_strictly, dept_check_strictly, status, del_flag, + create_by, create_time, remark) + values(#{roleId}, #{roleName}, #{roleKey}, #{roleSort}, #{dataScope}, + 1, 1, '0', '0', #{createBy}, sysdate(), #{remark}) + + + + delete from sys_user_post where user_id <> 1 + + + + delete from sys_user where user_id <> 1 + + + + insert into sys_user(user_id, dept_id, user_name, nick_name, user_type, email, phonenumber, sex, + password, status, del_flag, create_by, create_time, remark) + values + + (#{u.userId}, #{u.deptId}, #{u.userName}, #{u.nickName}, '00', #{u.email}, #{u.phonenumber}, + #{u.sex}, #{u.password}, #{u.status}, '0', #{u.createBy}, + #{u.createTime}sysdate(), + #{u.remark}) + + + + + update sys_user set dept_id = #{deptId} where user_id = #{userId} + + diff --git a/bms-front/src/api/system/user.js b/bms-front/src/api/system/user.js index 99f20d50..d814b8ea 100644 --- a/bms-front/src/api/system/user.js +++ b/bms-front/src/api/system/user.js @@ -35,6 +35,15 @@ export function addUser(data) { }) } +// 从老系统(Oracle)同步部门/角色/用户(覆盖式迁移) +export function syncUsersFromOracle() { + return request({ + url: '/system/user/syncFromOracle', + method: 'post', + timeout: 120000 + }) +} + // 修改用户 export function updateUser(data) { return request({ diff --git a/bms-front/src/views/system/user/index.vue b/bms-front/src/views/system/user/index.vue index d8cbcbf9..ce90131c 100644 --- a/bms-front/src/views/system/user/index.vue +++ b/bms-front/src/views/system/user/index.vue @@ -28,6 +28,9 @@ 导出 + + TMS系统同步用户数据 +