feat: 人资+机构同步

This commit is contained in:
2026-10-08 19:45:38 +08:00
parent 20bac2e078
commit 1650383670
12 changed files with 670 additions and 311 deletions
@@ -22,6 +22,7 @@ import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUnitService;
import com.budwk.app.sys.services.SysUserService;
import com.budwk.app.sys.utils.DataCenterUtil;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.AuthUtil;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -214,9 +215,9 @@ public class SysUnitController {
if (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
Cnd cnd = Cnd.NEW();
if (Strings.isBlank(pid)) {
cnd.and("parentId", "=", "").or("parentId", "is", null);
cnd.and("id", "=", DataCenterUtil.findRootUnit(sysUnitService.query(Cnd.NEW())).getId());
} else {
cnd.and("parentId", "=", pid);
cnd.and("parentId", "=", pid).and("id", "<>", pid);
}
cnd.asc("location").asc("path");
list = sysUnitService.query(cnd);
@@ -227,9 +228,9 @@ public class SysUnitController {
} else {
Cnd cnd = Cnd.NEW();
if (Strings.isBlank(pid)) {
cnd.and("parentId", "=", "").or("parentId", "is", null);
cnd.and("id", "=", DataCenterUtil.findRootUnit(sysUnitService.query(Cnd.NEW())).getId());
} else {
cnd.and("parentId", "=", pid);
cnd.and("parentId", "=", pid).and("id", "<>", pid);
}
cnd.asc("location").asc("path");
list = sysUnitService.query(cnd);
@@ -258,9 +259,9 @@ public class SysUnitController {
if (StpUtil.hasRole("SYSADMIN")) {
Cnd cnd = Cnd.NEW();
if (Strings.isBlank(pid)) {
cnd.and("parentId", "=", "").or("parentId", "is", null);
cnd.and("id", "=", DataCenterUtil.findRootUnit(sysUnitService.query(Cnd.NEW())).getId());
} else {
cnd.and("parentId", "=", pid);
cnd.and("parentId", "=", pid).and("id", "<>", pid);
}
cnd.asc("location").asc("path");
list = sysUnitService.query(cnd);
@@ -271,9 +272,9 @@ public class SysUnitController {
} else {
Cnd cnd = Cnd.NEW();
if (Strings.isBlank(pid)) {
cnd.and("parentId", "=", "").or("parentId", "is", null);
cnd.and("id", "=", DataCenterUtil.findRootUnit(sysUnitService.query(Cnd.NEW())).getId());
} else {
cnd.and("parentId", "=", pid);
cnd.and("parentId", "=", pid).and("id", "<>", pid);
}
cnd.asc("location").asc("path");
list = sysUnitService.query(cnd);
@@ -300,24 +301,23 @@ public class SysUnitController {
@SaCheckLogin
public Object tree(@Param("pid") String pid, HttpServletRequest req) {
try {
String rootId = StrUtil.blankToDefault(pid, "0");
String virtualRootId = "root";
Cnd cnd = Cnd.NEW();
cnd.asc("unitcode");
List<Sys_unit> list = sysUnitService.query(cnd);
String rootId = StrUtil.isBlank(pid) ? DataCenterUtil.findRootUnit(list).getId() : pid;
List<TreeNode<String>> nodeList = CollUtil.newArrayList();
for (int i = 0; i < list.size(); i++) {
Sys_unit unit = list.get(i);
/*
* 单位根节点存在 id 与 parentId 都为 0 的自引用数据。
* 构建树时把当前查询根挂到虚拟根下,避免“中国地质大学”和 parentId=0 的学院被构造成同级。
* 动态查询根挂到虚拟根下,兼容根节点父级为空或指向自身的数据。
*/
String parentId = rootId.equals(unit.getId()) ? virtualRootId : unit.getParentId();
nodeList.add(new TreeNode<>(unit.getId(), parentId, unit.getName(), i)
.setExtra(
Map.of(
"unitTypeCode", unit.getUnitTypeCode()
"unitTypeCode", unit.getUnitTypeCode() == null ? 0 : unit.getUnitTypeCode()
)
));
}
@@ -49,7 +49,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("性别")
@ColDefine(type = ColType.VARCHAR, width = 10)
@DataCenterColumn(name = "性别", key = "XB", dict = "USER_SEX")
@DataCenterColumn(name = "性别", key = "XBMC")
private String sex;
@Column
@@ -65,6 +65,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("出生日期")
@ColDefine(type = ColType.DATE)
@DataCenterColumn(name = "出生日期", key = "CSRQ")
private Date birthday;
@Column
@@ -108,12 +109,13 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("手机号码")
@ColDefine(type = ColType.VARCHAR, width = 32)
@DataCenterColumn(name = "手机号码", key = "YDDH")
@DataCenterColumn(name = "手机号码", key = "SJHM")
private String mobile;
@Column
@Comment("学历")
@ColDefine(type = ColType.VARCHAR, width = 20)
@DataCenterColumn(name = "学历", key = "ZGXLMC")
private String education;
@Column
@@ -124,7 +126,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("学位")
@ColDefine(type = ColType.VARCHAR, width = 50)
// @DataCenterColumn(name = "学位", key = "zgxwmc")
@DataCenterColumn(name = "学位", key = "ZGXWMC")
private String academicDegree;
@Column
@@ -135,6 +137,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("职称")
@ColDefine(type = ColType.VARCHAR, width = 50)
@DataCenterColumn(name = "职称", key = "ZYJSZWDMMC")
private String professionalTitle;
@Column
@@ -175,7 +178,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("来校时间")
@ColDefine(type = ColType.VARCHAR, width = 10)
@DataCenterColumn(name = "来校时间", key = "LXNY")
@DataCenterColumn(name = "来校时间", key = "LXRQ")
private String arrivalAtSchoolDate;
@Column
@@ -191,7 +194,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("在职状态")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "当前状态码", key = "DQZT", dict = "USER_STATE")
@DataCenterColumn(name = "在职状态", key = "DQZTMC")
private String userState;
@Column
@@ -208,7 +211,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("教职工类别码")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "教职工类别码", key = "RYLX", dict = "USER_PERSON_TYPE")
@DataCenterColumn(name = "人员类型", key = "YRFSDM", dict = "USER_PERSON_TYPE")
private String personType;
@Column
@@ -220,6 +223,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@Comment("编制类别码")
@ColDefine(type = ColType.VARCHAR, width = 30)
@DataCenterColumn(name = "编制类别", key = "BZLBDMMC")
private String preparedBy;
@Column
@@ -274,7 +278,7 @@ public class Sys_user extends BaseModel implements Serializable {
@Column
@ColDefine(type = ColType.VARCHAR, width = 32)
@DataCenterColumn(name = "单位", key = "SZDWH")
@DataCenterColumn(name = "单位", key = "SZDWDM")
private String unitId;
@Column
@@ -1,29 +1,25 @@
package com.budwk.app.sys.services.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.config.DataCenterProperties;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.services.SysDataUnitPullService;
import com.budwk.app.sys.utils.DataCenterUtil;
import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Dao;
import org.nutz.dao.Cnd;
import org.nutz.dao.Sqls;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -42,7 +38,6 @@ import java.util.stream.Collectors;
public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implements SysDataUnitPullService {
private static final String DMP_UNIT_KEY = "unit";
private static final int DMP_PAGE_SIZE = 1000;
@Inject
private DataCenterProperties dataCenterProperties;
@@ -64,58 +59,29 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateUnits(List<String> unitIds) {
String accessToken = getDmpAccessToken();
List<JSONObject> rawDataList = distinctDmpUnits(pullDmpUnits(getDmpUnitUrl(), accessToken));
Sys_unit root = DataCenterUtil.findRootUnit(query(Cnd.NEW()));
List<JSONObject> rawDataList = distinctDmpUnits(DataCenterUtil.pullData(getDmpUnitUrl()));
if (rawDataList.isEmpty()) {
throw new BaseException("当前未获取到单位数据,中止本次同步");
}
if (StrUtil.isBlank(root.getPath())) {
root.setPath(getUnitPath(""));
dao().updateIgnoreNull(root);
}
Set<String> parentCodes = rawDataList.stream()
.map(this::getNormalizedParentCode)
.map(raw -> getNormalizedParentCode(raw, root.getId()))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
rawDataList.stream()
.sorted(Comparator.comparingInt(this::getUnitLevel))
.forEach(raw -> saveOrUpdateUnit(raw, parentCodes));
.forEach(raw -> saveOrUpdateUnit(raw, parentCodes, root));
dao().execute(Sqls.create("update sys_unit u left join (select parentId, count(*) as childCount from sys_unit where parentId is not null and id<>parentId group by parentId) children on children.parentId=u.id set u.hasChildren=(children.childCount is not null)"));
log.info("信息中心单位数据同步完成,本次接口返回单位数量: {},待检查单位编码数量: {}", rawDataList.size(), unitIds == null ? 0 : unitIds.size());
}
/**
* 获取信息中心开放平台访问令牌。
* 参数来自 data-center.token-url、data-center.key、data-center.secret;
* 返回值为后续单位接口 body 中 access_token 使用的字符串。
*/
private String getDmpAccessToken() {
String tokenUrl = dataCenterProperties.getTokenUrl();
if (StrUtil.isBlank(tokenUrl)) {
throw new BaseException("未配置信息中心token地址: data-center.token-url");
}
String key = dataCenterProperties.getKey();
if (StrUtil.isBlank(key)) {
throw new BaseException("未配置信息中心token key: data-center.key");
}
String secret = dataCenterProperties.getSecret();
if (StrUtil.isBlank(secret)) {
throw new BaseException("未配置信息中心token secret: data-center.secret");
}
String params = URLUtil.buildQuery(Map.of("key", key, "secret", secret), StandardCharsets.UTF_8);
String tokenResBody = HttpUtil.createGet(tokenUrl + "?" + params).execute().body();
JSONObject tokenJsonBody = JSONUtil.parseObj(tokenResBody);
String accessToken = tokenJsonBody.getStr("access_token");
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("result") != null) {
accessToken = tokenJsonBody.getJSONObject("result").getStr("access_token");
}
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("data") != null) {
accessToken = tokenJsonBody.getJSONObject("data").getStr("access_token");
}
if (StrUtil.isBlank(accessToken)) {
accessToken = tokenJsonBody.getStr("token");
}
if (StrUtil.isBlank(accessToken)) {
throw new BaseException("获取信息中心token失败");
}
return accessToken;
}
/**
* 读取信息中心单位接口地址。
* 配置项为 data-center.urls.unit,返回值为单位全量接口URL。
@@ -128,53 +94,6 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
return url;
}
/**
* 分页拉取单位接口数据。
* 入参 url 为单位接口地址,accessToken 为信息中心令牌;
* 请求 body 包含 access_token、per_page、page;
* 返回值为 result.data 合并后的单位原始 JSON 列表。
*/
private List<JSONObject> pullDmpUnits(String url, String accessToken) {
List<JSONObject> rawDataList = new ArrayList<>();
int page = 1;
int maxPage = 1;
int total = 0;
do {
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.body(JSONUtil.toJsonStr(Map.of(
"access_token", accessToken,
"per_page", String.valueOf(DMP_PAGE_SIZE),
"page", String.valueOf(page)
)));
log.info("请求信息中心单位数据,第{}页: {}", page, httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求信息中心单位数据第{}页结果: {}", page, resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 10000) {
throw new BaseException("获取信息中心单位数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("message"));
}
JSONObject result = jsonBody.getJSONObject("result");
if (result == null) {
break;
}
total = result.getInt("total", total);
maxPage = result.getInt("max_page", maxPage);
JSONArray data = result.getJSONArray("data");
if (CollUtil.isNotEmpty(data)) {
rawDataList.addAll(data.stream().map(v -> (JSONObject) v).toList());
}
log.info("信息中心单位数据拉取进度: {}/{}", rawDataList.size(), total);
page++;
} while (page <= maxPage);
return rawDataList;
}
/**
* 按单位代码去重,避免接口分页或源数据重复导致同一 DWDM 重复写入。
*/
@@ -183,7 +102,10 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
for (JSONObject raw : rawDataList) {
String unitCode = raw.getStr("DWDM");
if (StrUtil.isBlank(unitCode)) {
continue;
throw new BaseException("单位接口存在缺少 DWDM 的记录");
}
if (raw.getInt("DWCC") == null || raw.getInt("DWCC") < 1) {
throw new BaseException("单位{}的 DWCC 无效", unitCode);
}
unitMap.putIfAbsent(unitCode, raw);
}
@@ -192,15 +114,18 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
/**
* 保存或更新单位。
* DWDM 对应系统单位 id/unitcode,DWMC 对应名称,LSDWH 对应父级单位号,DWCC 对应信息中心原始单位层级。
* DWDM 对应系统单位 id/unitcode,DWMC 对应名称,LSDWDM 对应父级单位号,DWCC 对应信息中心原始单位层级。
*/
private void saveOrUpdateUnit(JSONObject raw, Set<String> parentCodes) {
private void saveOrUpdateUnit(JSONObject raw, Set<String> parentCodes, Sys_unit root) {
String unitCode = raw.getStr("DWDM");
if (StrUtil.isBlank(unitCode)) {
return;
}
String parentId = getParentId(unitCode, getNormalizedParentCode(raw));
if (root.getId().equals(unitCode)) {
throw new BaseException("接口单位{}与本地顶级机构编号冲突,中止同步", unitCode);
}
String parentId = getParentId(unitCode, getNormalizedParentCode(raw, root.getId()), root.getId());
Sys_unit unit = fetch(unitCode);
if (unit == null) {
unit = new Sys_unit();
@@ -210,26 +135,37 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
setUnitValue(unit, raw, parentId, parentCodes);
dao().fastInsert(unit);
} else {
setUnitValue(unit, raw, parentId, parentCodes);
if (StrUtil.isBlank(unit.getPath())) {
unit.setPath(getUnitPath(parentId));
if (!parentId.equals(unit.getParentId()) || StrUtil.isBlank(unit.getPath())) {
String oldPath = unit.getPath();
String newPath = getUnitPath(parentId);
if (StrUtil.isNotBlank(oldPath)) {
if (newPath.startsWith(oldPath)) {
throw new BaseException("单位{}不能移动到自身的下级单位", unitCode);
}
dao().execute(Sqls.create("update sys_unit set path=concat(@newPath, substring(path, @start)) where path like @prefix")
.setParam("newPath", newPath).setParam("start", oldPath.length() + 1).setParam("prefix", oldPath + "%"));
dao().execute(Sqls.create("update sys_user set unitPath=concat(@newPath, substring(unitPath, @start)) where unitPath like @prefix")
.setParam("newPath", newPath).setParam("start", oldPath.length() + 1).setParam("prefix", oldPath + "%"));
dao().execute(Sqls.create("update sys_user_source set unitPath=concat(@newPath, substring(unitPath, @start)) where unitPath like @prefix")
.setParam("newPath", newPath).setParam("start", oldPath.length() + 1).setParam("prefix", oldPath + "%"));
}
unit.setPath(newPath);
}
setUnitValue(unit, raw, parentId, parentCodes);
dao().updateIgnoreNull(unit);
}
}
/**
* 根据接口字段给系统单位赋值。
* DWYXBS 表示单位是否有效,DWCC 表示信息中心原始单位层次;
* DWCC 表示信息中心原始单位层次;
* 系统内置根单位占用一级层级,因此保存时统一将 DWCC 加一。
*/
private void setUnitValue(Sys_unit unit, JSONObject raw, String parentId, Set<String> parentCodes) {
String unitCode = raw.getStr("DWDM");
String unitName = raw.getStr("DWMC");
String aliasName = StrUtil.blankToDefault(raw.getStr("DWJC"), unitName);
Integer rawUnitLevel = raw.getInt("DWCC");
Integer unitLevel = getNormalizedUnitLevel(raw);
boolean availableBusinessUnit = "是".equals(raw.getStr("DWYXBS")) && Integer.valueOf(2).equals(rawUnitLevel);
unit.setParentId(parentId);
unit.setName(unitName);
@@ -237,24 +173,34 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
unit.setUnitcode(unitCode);
unit.setAddress(raw.getStr("DWDZ"));
unit.setUnitLevel(unitLevel);
unit.setUnitType(raw.getStr("DWLB"));
unit.setUnitTypeCode(availableBusinessUnit ? 1 : 0);
unit.setUnitType(raw.getStr("DWLBDM"));
unit.setUnitTypeCode(1);
unit.setHasChildren(parentCodes.contains(unitCode));
}
/**
* 计算父级单位号。
* LSDWH 为空、等于自身或本地不存在该父级时按根级处理;系统根单位 0 允许直接作为父级。
* 异常父级仅允许沿用本地已修正的关系,并确认父级链最终到达顶级机构。
*/
private String getParentId(String unitCode, String parentCode) {
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode)) {
return "";
private String getParentId(String unitCode, String parentCode, String rootId) {
if (StrUtil.isBlank(parentCode) || unitCode.equals(parentCode) || fetch(parentCode) == null) {
Sys_unit existing = fetch(unitCode);
parentCode = existing == null ? null : existing.getParentId();
}
if ("0".equals(parentCode)) {
return parentCode;
String ancestorId = parentCode;
Set<String> visited = new HashSet<>();
visited.add(unitCode);
while (!rootId.equals(ancestorId)) {
if (StrUtil.isBlank(ancestorId) || !visited.add(ancestorId)) {
throw new BaseException("单位{}的上级机构无效或存在循环,无法确定归属", unitCode);
}
Sys_unit ancestor = fetch(ancestorId);
if (ancestor == null) {
throw new BaseException("单位{}的上级机构{}不存在", unitCode, ancestorId);
}
ancestorId = ancestor.getParentId();
}
Sys_unit parentUnit = fetch(parentCode);
return parentUnit == null ? "" : parentCode;
return parentCode;
}
/**
@@ -265,20 +211,23 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl<Sys_unit> implem
String parentPath = "";
if (StrUtil.isNotBlank(parentId)) {
Sys_unit parentUnit = fetch(parentId);
parentPath = parentUnit == null ? "" : parentUnit.getPath();
if (parentUnit == null || StrUtil.isBlank(parentUnit.getPath())) {
throw new BaseException("父机构{}不存在或路径为空", parentId);
}
parentPath = parentUnit.getPath();
}
return getSubPath("sys_unit", "path", parentPath);
}
/**
* 信息中心一级单位应挂在系统根单位 0 下,其余单位沿用接口返回的 LSDWH。
* 信息中心一级单位挂在本地顶级机构下,其余单位沿用接口返回的 LSDWDM。
*/
private String getNormalizedParentCode(JSONObject raw) {
private String getNormalizedParentCode(JSONObject raw, String rootId) {
Integer rawUnitLevel = raw.getInt("DWCC");
if (Integer.valueOf(1).equals(rawUnitLevel)) {
return "0";
return rootId;
}
return raw.getStr("LSDWH");
return raw.getStr("LSDWDM");
}
/**
@@ -133,7 +133,7 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
// 获取单位信息,后续判断单位变更
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.where("unitTypeCode", "=", "1"));
List<Sys_unit> unitList = dao.query(Sys_unit.class, Cnd.NEW());
unitIdNameMap = unitList.stream().collect(Collectors.toMap(Sys_unit::getId, Sys_unit::getName));
// 查询数据源
@@ -3,10 +3,9 @@ package com.budwk.app.sys.services.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.AsyncUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.event.role.RoleEventMsg;
import com.budwk.app.base.event.role.RoleEventPublisher;
import com.budwk.app.base.utils.ConditionGroupUtil;
@@ -15,6 +14,8 @@ import com.budwk.app.sys.models.*;
import com.budwk.app.sys.param.SysDataUserUpdateParam;
import com.budwk.app.sys.services.SysDataUserUpdateService;
import com.budwk.app.sys.services.SysRoleService;
import com.budwk.app.sys.services.SysUnitService;
import com.budwk.app.sys.utils.DataCenterUtil;
import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
@@ -28,7 +29,7 @@ import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.random.R;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.ArrayList;
/**
* 增量更新系统用户数据
@@ -40,6 +41,8 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
private Dao dao;
@Inject
private SysRoleService sysRoleService;
@Inject
private SysUnitService sysUnitService;
@Override
@Aop(TransAop.READ_COMMITTED)
@@ -58,6 +61,43 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
// 执行查询
List<Sys_user_source> userSources = dao.query(Sys_user_source.class, cnd);
// 先补齐机构,再写入人员;补建机构统一挂到本地唯一顶级机构。
List<Sys_unit> sysUnits = dao.query(Sys_unit.class, Cnd.NEW());
var unitMap = sysUnits.stream().collect(java.util.stream.Collectors.toMap(Sys_unit::getId, v -> v));
List<Sys_unit> insertUnits = new ArrayList<>();
Sys_unit root = null;
for (Sys_user_source source : userSources) {
if (StrUtil.isBlank(source.getUnitId())) {
throw new BaseException("人员{}缺少所属机构编号", source.getLoginname());
}
Sys_unit unit = unitMap.get(source.getUnitId());
if (unit == null) {
if (StrUtil.isBlank(source.getUnitName())) {
throw new BaseException("人员{}所属机构{}缺少名称,无法补建", source.getLoginname(), source.getUnitId());
}
if (root == null) {
root = DataCenterUtil.findRootUnit(sysUnits);
if (StrUtil.isBlank(root.getPath())) {
root.setPath(sysUnitService.getSubPath("sys_unit", "path", ""));
dao.updateIgnoreNull(root);
}
}
unit = new Sys_unit();
unit.setUnitcode(source.getUnitId());
unit.setName(source.getUnitName());
unit.setUnitLevel(2);
unit.setUnitTypeCode(1);
sysUnitService.save(unit, root.getId());
unitMap.put(unit.getId(), unit);
insertUnits.add(unit);
}
if (StrUtil.isBlank(unit.getPath())) {
throw new BaseException("人员{}所属机构{}的路径为空", source.getLoginname(), unit.getId());
}
source.setUnitName(unit.getName());
source.setUnitPath(unit.getPath());
}
//加入到系统用户表
List<Sys_user> sysUsers = BeanUtil.copyToList(userSources, Sys_user.class);
for (Sys_user sysUser : sysUsers) {
@@ -72,9 +112,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
}
List<List<Sys_user>> splitSysUsers = ListUtil.split(sysUsers, 500);
List<CompletableFuture<List<Sys_user>>> splitSysUsersFutures = splitSysUsers.stream().map(v -> CompletableFuture.supplyAsync(() -> dao.fastInsert(v))).toList();
//等待任务全部完成
AsyncUtil.waitAll(CompletableFuture.allOf(splitSysUsersFutures.toArray(new CompletableFuture[0])));
for (List<Sys_user> batch : splitSysUsers) {
dao.fastInsert(batch);
}
// 增量更新只处理新增人员,需要同步写入用户历史记录;列表的变更类型、变更时间通过 sys_user_history 最新记录展示。
List<Sys_user_history> histories = BeanUtil.copyToList(userSources, Sys_user_history.class);
@@ -85,8 +125,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
history.setChangeOrigin(MemberChangeOrigin.SYSTEM.name());
}
List<List<Sys_user_history>> splitHistories = ListUtil.split(histories, 500);
List<CompletableFuture<List<Sys_user_history>>> splitHistoryFutures = splitHistories.stream().map(v -> CompletableFuture.supplyAsync(() -> dao.fastInsert(v))).toList();
AsyncUtil.waitAll(CompletableFuture.allOf(splitHistoryFutures.toArray(new CompletableFuture[0])));
for (List<Sys_user_history> batch : splitHistories) {
dao.fastInsert(batch);
}
Sys_role publicRole = sysRoleService.getByCode(RoleConstant.PUBLIC);
@@ -97,38 +138,9 @@ public class SysDataUserIncrUpdateServiceImpl implements SysDataUserUpdateServic
sysUserRole.setRoleId(publicRole.getId());
return sysUserRole;
}).toList();
ThreadUtil.execAsync(() -> {
dao.insert(sysUserRoles);
});
dao.insert(sysUserRoles);
sysRoleService.clearCache();
//比较单位数据
List<Sys_unit> sysUnits = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getUnitLevel, "=", 2));
List<String> sysUnitIds = sysUnits.stream().map(Sys_unit::getId).toList();
//信息中心的数据 转为单位代码 ->单位名称 map
var unitIdNameMap = userSources.stream()
.filter(v -> StrUtil.isAllNotBlank(v.getUnitId(), v.getUnitName()))
.collect(java.util.stream.Collectors.toMap(
Sys_user_source::getUnitId,
Sys_user_source::getUnitName,
(existingValue, newValue) -> newValue));
//需要新增的单位
List<Sys_unit> insertUnits = unitIdNameMap.entrySet().stream()
.filter(entry -> !sysUnitIds.contains(entry.getKey()))
.map(entry -> {
Sys_unit sysUnit = new Sys_unit();
sysUnit.setId(entry.getKey());
sysUnit.setName(entry.getValue());
sysUnit.setParentId("1");
sysUnit.setUnitLevel(2);
return sysUnit;
}).toList();
ThreadUtil.execAsync(() -> {
dao.insert(insertUnits);
});
// 6.1 更新提案的校领导角色,发送订阅
ProposalConfig config = dao.fetch(ProposalConfig.class, Cnd.where("delFlag", "=", false).desc("updatedAt"));
for (String unitId : config.getSchoolLeaderUnitIds()) {
@@ -52,8 +52,6 @@ import java.util.stream.Collectors;
public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source> implements SysDataUserPullService {
private static final String DMP_TEACHER_KEY = "teacher";
private static final String DMP_DISPATCH_KEY = "dispatch";
private static final int DMP_PAGE_SIZE = 1000;
@Inject
private SysDictService sysDictService;
@@ -120,21 +118,17 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
// 字典码表Map,key为父级编码,value为子级字典列表
Map<String, List<Sys_data_dict>> dataDictMap = dataDictList.stream().collect(Collectors.groupingBy(Sys_data_dict::getParentCode));
String accessToken = getDmpAccessToken();
List<JSONObject> rawDataList = new ArrayList<>();
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_TEACHER_KEY), accessToken, "在岗教职工"));
rawDataList.addAll(pullDmpUsers(getDmpUrl(DMP_DISPATCH_KEY), accessToken, "在岗劳务派遣"));
rawDataList = distinctDmpUsers(rawDataList);
List<JSONObject> rawDataList = distinctDmpUsers(DataCenterUtil.pullData(getDmpUrl(DMP_TEACHER_KEY)));
if (rawDataList.isEmpty()) {
log.warn("当前未获取到人员数据");
throw new BaseException("当前未获取到人员数据,中止本次同步");
}
Date nowDate = new Date();
// 人员单位号先与单位表批量比对,缺失单位只同步一次,避免逐人查询单位表。
Set<String> sourceUnitIds = rawDataList.stream()
.map(raw -> raw.getStr("SZDWH"))
.map(raw -> raw.getStr("SZDWDM"))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
Map<String, Sys_unit> unitMap = dao().query(Sys_unit.class, Cnd.NEW()).stream()
@@ -146,25 +140,34 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
}
// 按单位表的实际等级判断:三级单位为3、直属二级单位为2;单位号位数不参与判断。
String rootId = DataCenterUtil.findRootUnit(new ArrayList<>(unitMap.values())).getId();
// 深层机构沿父级查找二级单位,并保留所属三级单位。
Map<String, String> tertiaryParentUnitIds = new HashMap<>();
Map<String, String> tertiaryUnitIds = new HashMap<>();
for (String sourceUnitId : sourceUnitIds) {
Sys_unit unit = unitMap.get(sourceUnitId);
if (unit == null) {
log.warn("人员单位号{}在单位表中不存在,保留信息中心原始单位号", sourceUnitId);
continue;
throw new BaseException("人员单位号{}在机构同步后仍不存在,中止本次同步", sourceUnitId);
}
if (!Integer.valueOf(3).equals(unit.getUnitLevel())) {
continue;
Set<String> visited = new HashSet<>();
while (!rootId.equals(unit.getId()) && !Integer.valueOf(2).equals(unit.getUnitLevel())) {
if (!visited.add(unit.getId())) {
throw new BaseException("人员单位{}存在循环父级", sourceUnitId);
}
if (Integer.valueOf(3).equals(unit.getUnitLevel())) {
tertiaryUnitIds.put(sourceUnitId, unit.getId());
}
unit = unitMap.get(unit.getParentId());
if (unit == null || rootId.equals(unit.getId())) {
throw new BaseException("人员单位{}缺少有效的二级上级机构", sourceUnitId);
}
}
Sys_unit parentUnit = unitMap.get(unit.getParentId());
if (parentUnit == null || !Integer.valueOf(2).equals(parentUnit.getUnitLevel())) {
log.warn("三级单位{}缺少有效的二级父单位,保留信息中心原始单位号", sourceUnitId);
continue;
if (!sourceUnitId.equals(unit.getId())) {
tertiaryParentUnitIds.put(sourceUnitId, unit.getId());
}
tertiaryParentUnitIds.put(sourceUnitId, parentUnit.getId());
}
Map<String, Sys_unit> resolvedUnitMap = unitMap;
// 赋值
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
Sys_user_source sysUser = new Sys_user_source();
@@ -182,35 +185,44 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
.findFirst().orElse(new Sys_data_dict());
mapping.field.set(sysUser, StrUtil.blankToDefault(sysDataDict.getName(), raw.getStr(mapping.key)));
} else {
// 特殊处理 根据身份证号获取性别和出生年月
if ("SFZJH".equals(mapping.key)) {
String idCard = raw.getStr(mapping.key);
mapping.field.set(sysUser, idCard);
// 设置出生年月
try {
sysUser.setBirthday(IdcardUtil.getBirthDate(idCard));
} catch (Exception e) {
sysUser.setBirthday(null);
}
} else if ("LXNY".equals(mapping.key)) {
if ("LXRQ".equals(mapping.key)) {
mapping.field.set(sysUser, normalizeArrivalAtSchoolDate(raw.getStr(mapping.key)));
} else {
mapping.field.set(sysUser, raw.getStr(mapping.key));
}
}
} else if (mapping.field.getType() == Date.class) {
mapping.field.set(sysUser, raw.getDate(mapping.key));
String dateText = StrUtil.trim(raw.getStr(mapping.key));
if (StrUtil.isNotBlank(dateText)) {
mapping.field.set(sysUser, dateText.matches("\\d{8}")
? DateUtil.parse(dateText, "yyyyMMdd") : DateUtil.parse(dateText));
}
}
} catch (IllegalAccessException e) {
log.error("设置字段值失败: {}", mapping.field.getName(), e);
}
}
sysUser.setUnitName(raw.getStr("SZDW"));
String sourceUnitId = raw.getStr("SZDWH");
if (sysUser.getBirthday() == null && StrUtil.isNotBlank(sysUser.getIdCard())) {
try {
sysUser.setBirthday(IdcardUtil.getBirthDate(sysUser.getIdCard()));
} catch (Exception e) {
sysUser.setBirthday(null);
}
}
String sourceUnitId = raw.getStr("SZDWDM");
if (StrUtil.isBlank(sourceUnitId)) {
throw new BaseException("人员{}缺少所属机构编号 SZDWDM", sysUser.getLoginname());
}
String parentUnitId = tertiaryParentUnitIds.get(sourceUnitId);
sysUser.setUnitId(StrUtil.blankToDefault(parentUnitId, sourceUnitId));
sysUser.setThreeUnitId(parentUnitId == null ? null : sourceUnitId);
sysUser.setThreeUnitId(tertiaryUnitIds.get(sourceUnitId));
Sys_unit unit = resolvedUnitMap.get(sysUser.getUnitId());
if (unit == null || StrUtil.isBlank(unit.getPath())) {
throw new BaseException("人员{}所属机构不存在或路径为空", sysUser.getLoginname());
}
sysUser.setUnitName(unit.getName());
sysUser.setUnitPath(unit.getPath());
// 设置拉取时间
sysUser.setPullTime(nowDate);
@@ -232,7 +244,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
/**
* 规范化信息中心返回的来校时间。
* 入参 arrivalAtSchoolDate 对应信息中心 LXNY 字段,允许 yyyy-MM-dd 或 yyyy-MM;
* 入参 arrivalAtSchoolDate 对应信息中心 LXRQ 字段,允许 yyyy-MM-dd 或 yyyy-MM;
* 返回值用于 sys_user_source.arrivalAtSchoolDate 和后续 sys_user.arrivalAtSchoolDate,年月格式统一补为当月 01 日。
*/
private String normalizeArrivalAtSchoolDate(String arrivalAtSchoolDate) {
@@ -248,48 +260,8 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
String[] parts = dateText.split("-");
return parts[0] + "-" + StrUtil.padPre(parts[1], 2, '0') + "-01";
}
return dateText;
}
/**
* 获取信息中心开放平台访问令牌。
* 返回值为接口后续调用使用的 access_token 字符串。
*/
private String getDmpAccessToken() {
String tokenUrl = dataCenterProperties.getTokenUrl();
if (StrUtil.isBlank(tokenUrl)) {
throw new BaseException("未配置信息中心token地址: data-center.token-url");
}
String key = dataCenterProperties.getKey();
if (StrUtil.isBlank(key)) {
throw new BaseException("未配置信息中心token key: data-center.key");
}
String secret = dataCenterProperties.getSecret();
if (StrUtil.isBlank(secret)) {
throw new BaseException("未配置信息中心token secret: data-center.secret");
}
// 信息中心token接口要求key和secret通过URL参数传入,配置中分开维护,调用时统一组装。
String params = URLUtil.buildQuery(Map.of("key", key, "secret", secret), StandardCharsets.UTF_8);
HttpRequest tokenHttpRequest = HttpUtil.createGet(tokenUrl + "?" + params);
log.info("请求信息中心token: {}", tokenHttpRequest);
String tokenResBody = tokenHttpRequest.execute().body();
log.info("请求信息中心token结果: {}", tokenResBody);
JSONObject tokenJsonBody = JSONUtil.parseObj(tokenResBody);
String accessToken = tokenJsonBody.getStr("access_token");
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("result") != null) {
accessToken = tokenJsonBody.getJSONObject("result").getStr("access_token");
}
if (StrUtil.isBlank(accessToken) && tokenJsonBody.getJSONObject("data") != null) {
accessToken = tokenJsonBody.getJSONObject("data").getStr("access_token");
}
if (StrUtil.isBlank(accessToken)) {
accessToken = tokenJsonBody.getStr("token");
}
if (StrUtil.isBlank(accessToken)) {
throw new BaseException("获取信息中心token失败");
}
return accessToken;
return DateUtil.formatDate(dateText.matches("\\d{8}")
? DateUtil.parse(dateText, "yyyyMMdd") : DateUtil.parse(dateText));
}
/**
@@ -305,54 +277,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
}
/**
* 分页拉取单个人员接口数据。
* 入参 url 为人员接口地址,accessToken 为信息中心令牌,sourceName 用于日志区分接口来源。
* 返回值为该接口所有分页合并后的原始人员 JSON 列表。
*/
private List<JSONObject> pullDmpUsers(String url, String accessToken, String sourceName) {
List<JSONObject> rawDataList = new ArrayList<>();
int page = 1;
int maxPage = 1;
int total = 0;
do {
// 信息中心接口单页最多返回1000条,按page循环拉完当前接口全部数据。
HttpRequest httpRequest = HttpUtil.createPost(url);
httpRequest.header(Header.CONTENT_TYPE, "application/json");
httpRequest.body(JSONUtil.toJsonStr(Map.of(
"access_token", accessToken,
"per_page", String.valueOf(DMP_PAGE_SIZE),
"page", String.valueOf(page)
)));
log.info("请求{}人员数据,第{}页: {}", sourceName, page, httpRequest);
String resBody = httpRequest.execute().body();
log.info("请求{}人员数据第{}页结果: {}", sourceName, page, resBody);
JSONObject jsonBody = JSONUtil.parseObj(resBody);
if (jsonBody.getInt("code") != 10000) {
throw new BaseException("获取" + sourceName + "人员数据失败,错误码: " + jsonBody.getInt("code") + ";错误原因:" + jsonBody.getStr("message"));
}
JSONObject result = jsonBody.getJSONObject("result");
if (result == null) {
break;
}
total = result.getInt("total", total);
maxPage = result.getInt("max_page", maxPage);
JSONArray data = result.getJSONArray("data");
if (CollUtil.isNotEmpty(data)) {
rawDataList.addAll(data.stream().map(v -> (JSONObject) v).toList());
}
log.info("{}人员数据拉取进度: {}/{}", sourceName, rawDataList.size(), total);
page++;
} while (page <= maxPage);
return rawDataList;
}
/**
* 合并两个接口数据时按职工号去重,避免同一批次出现重复人员。
* 按职工号去重,避免同一批次出现重复人员。
* 返回值保持第一次出现的数据,用于后续统一映射入库。
*/
private List<JSONObject> distinctDmpUsers(List<JSONObject> rawDataList) {
@@ -360,7 +285,7 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
for (JSONObject raw : rawDataList) {
String loginName = raw.getStr("ZGH");
if (StrUtil.isBlank(loginName)) {
loginName = raw.getStr("ID");
throw new BaseException("人员接口存在缺少职工号 ZGH 的记录");
}
userMap.putIfAbsent(loginName, raw);
}
@@ -3,9 +3,18 @@ package com.budwk.app.sys.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.sys.models.Sys_unit;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
@@ -17,6 +26,50 @@ import java.util.Map;
*/
public class DataCenterUtil {
public static Sys_unit findRootUnit(List<Sys_unit> units) {
List<Sys_unit> roots = units.stream().filter(unit ->
StrUtil.isBlank(unit.getParentId())
|| (unit.getId().equals(unit.getParentId())
&& (unit.getUnitLevel() == null || unit.getUnitLevel() <= 1))).toList();
if (roots.size() != 1) {
throw new BaseException("本地顶级机构数量为{},无法确定唯一的学校根机构", roots.size());
}
return roots.get(0);
}
public static List<JSONObject> pullData(String url) {
JSONObject firstResponse = requestData(url, Map.of("", ""));
JSONObject pageInfo = firstResponse.getJSONObject("pageInfo");
Integer pages = pageInfo == null ? null : pageInfo.getInt("pages");
if (pages == null || pages < 0) {
throw new BaseException("数据中心接口未返回有效的 pageInfo.pages");
}
List<JSONObject> records = new ArrayList<>();
for (int page = 1; page <= pages; page++) {
JSONObject response = requestData(url, Map.of("pageNum", page));
JSONArray data = response.getJSONArray("dataSet");
if (data == null) {
throw new BaseException("数据中心接口第{}页缺少 dataSet", page);
}
records.addAll(data.toList(JSONObject.class));
}
return records;
}
private static JSONObject requestData(String url, Map<String, ?> body) {
try (HttpResponse response = HttpUtil.createPost(url)
.body(JSONUtil.toJsonStr(body)).execute()) {
if (!response.isOk()) {
throw new BaseException("数据中心接口请求失败,HTTP状态码:{}", response.getStatus());
}
JSONObject result = JSONUtil.parseObj(response.body());
if (!Integer.valueOf(1).equals(result.getInt("returnStatus"))) {
throw new BaseException("数据中心接口返回失败:{}", result.getStr("errorMsg"));
}
return result;
}
}
/**
* 映射字段
* @param obj
+2 -2
View File
@@ -61,8 +61,8 @@ custom:
password: 3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD
token-url: https://bd.whcp.zjnu.edu.cn/sjjkfwpt/api/bd-api/oauth/token
urls:
teacher: https://bd.whcp.zjnu.edu.cn/sjjkfwpt/api/bd-api/get/JZGJCSJZLB
unit: https://bd.whcp.zjnu.edu.cn/sjjkfwpt/api/bd-api/get/YXSDWJBSJZLB
teacher: http://mdm.zjnu.edu.cn/do/api/call/lxyxt_jzgjbxx
unit: http://mdm.zjnu.edu.cn/do/api/call/lxyxt_dwxx
msg:
platform:
enabled: false
+2 -2
View File
@@ -61,8 +61,8 @@ custom:
password: 3848A3C738EFBB2C2E76E0D59B419DACA16A80F3AB0354CD
token-url: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/oauth/token
urls:
teacher: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/JZGJCSJZLB
unit: https://bd.whcp.edu.cn/sjjkfwpt/api/bd-api/get/YXSDWJBSJZLB
teacher: http://mdm.zjnu.edu.cn/do/api/call/lxyxt_jzgjbxx
unit: http://mdm.zjnu.edu.cn/do/api/call/lxyxt_dwxx
msg:
platform:
enabled: false
@@ -0,0 +1,138 @@
-- 目标:浙江师范大学 -> 下级单位,不再显示额外的“组织机构”层。
-- 当前 v4 接口固定以 ID=0 构建树,因此将学校 ID=1 的信息合并到 ID=0。
-- 只修改 sys_unit;不修改人员、角色和业务数据。执行期间不要并发写入相关数据。
-- 检查其他表的机构 ID、机构 ID 数组及显式外键;存在对 ID=1 的引用时停止。
-- 备份表固定命名,不覆盖已有备份。本文件独立执行,不要嵌入已有事务。
DELIMITER $$
DROP PROCEDURE IF EXISTS zhgh_zjnu.merge_zjnu_school_root$$
CREATE PROCEDURE zhgh_zjnu.merge_zjnu_school_root()
BEGIN
DECLARE finished BOOLEAN DEFAULT FALSE;
DECLARE reference_table VARCHAR(64);
DECLARE reference_column VARCHAR(64);
DECLARE is_array BOOLEAN;
DECLARE error_message VARCHAR(128);
DECLARE root_has_children BOOLEAN;
DECLARE references_cursor CURSOR FOR
SELECT c.TABLE_NAME, c.COLUMN_NAME,
LOWER(c.COLUMN_NAME) LIKE '%unitids'
FROM information_schema.COLUMNS c
JOIN information_schema.TABLES t
ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
WHERE c.TABLE_SCHEMA = 'zhgh_zjnu' AND t.TABLE_TYPE = 'BASE TABLE'
AND c.TABLE_NAME <> 'sys_unit' AND LEFT(c.TABLE_NAME, 4) <> 'bak_'
AND (LOWER(c.COLUMN_NAME) LIKE '%unitid'
OR LOWER(c.COLUMN_NAME) LIKE '%unitids'
OR LOWER(c.COLUMN_NAME) = 'undertakeid'
OR EXISTS (
SELECT 1 FROM information_schema.KEY_COLUMN_USAGE k
WHERE k.TABLE_SCHEMA = c.TABLE_SCHEMA AND k.TABLE_NAME = c.TABLE_NAME
AND k.COLUMN_NAME = c.COLUMN_NAME
AND k.REFERENCED_TABLE_SCHEMA = 'zhgh_zjnu'
AND k.REFERENCED_TABLE_NAME = 'sys_unit'
AND k.REFERENCED_COLUMN_NAME = 'id'));
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
IF NOT EXISTS (SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'zhgh_zjnu' AND TABLE_NAME = 'sys_unit' AND ENGINE = 'InnoDB') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'sys_unit 必须使用 InnoDB';
END IF;
IF NOT EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit
WHERE id = '1' AND name = '浙江师范大学' AND parentId = '0')
OR NOT EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit
WHERE id = '0' AND name = '组织机构') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '当前根节点与指定数据不一致,停止合并';
END IF;
IF EXISTS (SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'zhgh_zjnu' AND TABLE_NAME = 'bak_sys_unit_before_school_root_merge') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '学校根节点合并备份已存在,不覆盖原备份';
END IF;
OPEN references_cursor;
reference_loop: LOOP
FETCH references_cursor INTO reference_table, reference_column, is_array;
IF finished THEN
LEAVE reference_loop;
END IF;
SET @school_root_reference = 0;
SET @school_root_check_sql = CONCAT(
'SELECT EXISTS (SELECT 1 FROM `zhgh_zjnu`.`', REPLACE(reference_table, '`', '``'),
'` WHERE ',
IF(is_array,
CONCAT('JSON_CONTAINS(`', REPLACE(reference_column, '`', '``'),
'`, JSON_QUOTE(''1'')) OR JSON_CONTAINS(`',
REPLACE(reference_column, '`', '``'), '`, ''1'')'),
CONCAT('CAST(`', REPLACE(reference_column, '`', '``'), '` AS CHAR) = ''1''')),
') INTO @school_root_reference');
PREPARE school_root_check_stmt FROM @school_root_check_sql;
EXECUTE school_root_check_stmt;
DEALLOCATE PREPARE school_root_check_stmt;
IF @school_root_reference = 1 THEN
SET error_message = LEFT(CONCAT('学校 ID=1 仍被引用:', reference_table, '.', reference_column), 128);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = error_message;
END IF;
END LOOP;
CLOSE references_cursor;
CREATE TABLE zhgh_zjnu.bak_sys_unit_before_school_root_merge LIKE zhgh_zjnu.sys_unit;
INSERT INTO zhgh_zjnu.bak_sys_unit_before_school_root_merge SELECT * FROM zhgh_zjnu.sys_unit;
DROP TEMPORARY TABLE IF EXISTS zhgh_zjnu.tmp_school_root_merge;
CREATE TEMPORARY TABLE zhgh_zjnu.tmp_school_root_merge LIKE zhgh_zjnu.sys_unit;
START TRANSACTION;
INSERT INTO zhgh_zjnu.tmp_school_root_merge SELECT * FROM zhgh_zjnu.sys_unit WHERE id = '1';
-- 先调整下级,再删除已暂存的学校记录;现有 ID=0 本身不删除。
UPDATE zhgh_zjnu.sys_unit SET parentId = '0' WHERE parentId = '1';
DELETE FROM zhgh_zjnu.sys_unit WHERE id = '1';
SELECT EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit WHERE parentId = '0' AND id <> '0')
INTO root_has_children;
UPDATE zhgh_zjnu.sys_unit target
JOIN zhgh_zjnu.tmp_school_root_merge school ON school.id = '1'
SET target.parentId = '0',
target.path = school.path,
target.name = school.name,
target.aliasName = school.aliasName,
target.unitcode = school.unitcode,
target.note = school.note,
target.address = school.address,
target.telephone = school.telephone,
target.email = school.email,
target.website = school.website,
target.location = school.location,
target.hasChildren = root_has_children,
target.logo = school.logo,
target.unitLevel = school.unitLevel,
target.unionId = school.unionId,
target.unionGroupId = school.unionGroupId,
target.createdBy = school.createdBy,
target.createdAt = school.createdAt,
target.updatedBy = school.updatedBy,
target.updatedAt = school.updatedAt,
target.delFlag = school.delFlag,
target.divisionCollegeCode = school.divisionCollegeCode,
target.institutionType = school.institutionType,
target.unitType = school.unitType,
target.unitTypeCode = 0
WHERE target.id = '0';
COMMIT;
DROP TEMPORARY TABLE zhgh_zjnu.tmp_school_root_merge;
END$$
CALL zhgh_zjnu.merge_zjnu_school_root()$$
DROP PROCEDURE zhgh_zjnu.merge_zjnu_school_root$$
DELIMITER ;
@@ -0,0 +1,87 @@
-- 修复 v3 机构导入后 v4 单位树查询失败的问题,只操作 v4 的 sys_unit。
-- 目标库:zhgh_zjnu。独立执行本文件,期间不要并发修改机构数据。
-- 补齐 unitTypeCode;缺少 ID=0 的根节点时新增“组织机构”容器。
-- 保留现有机构 ID、path、工会关系及人员数据,原顶层机构挂到 ID=0 下。
-- 备份表不覆盖;存在同名备份时停止,防止覆盖首次修复前的数据。
DELIMITER $$
DROP PROCEDURE IF EXISTS zhgh_zjnu.repair_v4_unit_tree_after_v3_import$$
CREATE PROCEDURE zhgh_zjnu.repair_v4_unit_tree_after_v3_import()
BEGIN
DECLARE root_has_children BOOLEAN DEFAULT FALSE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
IF NOT EXISTS (SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'zhgh_zjnu' AND TABLE_NAME = 'sys_unit' AND ENGINE = 'InnoDB') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '目标 sys_unit 必须存在且使用 InnoDB';
END IF;
IF NOT EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '机构表为空,无法修复导入的单位树';
END IF;
IF EXISTS (SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'zhgh_zjnu' AND TABLE_NAME = 'bak_sys_unit_before_tree_repair') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '单位树修复备份已存在,本次停止,不覆盖原备份';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit WHERE id = 'root') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '机构 ID=root 与接口虚拟根冲突,需明确 ID 映射后再处理';
END IF;
IF NOT EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit WHERE id = '0')
AND EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit WHERE unitcode = '0') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '机构编码 0 已被其他机构使用,停止新增根节点';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit child
LEFT JOIN zhgh_zjnu.sys_unit parent ON parent.id = child.parentId
WHERE child.id <> '0' AND COALESCE(TRIM(child.parentId), '') NOT IN ('', '0')
AND (parent.id IS NULL OR child.id = child.parentId)) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '存在缺失的父机构或非根节点自引用,不能自动确定归属';
END IF;
-- 建表放在修复事务之前;完整保留修复前机构数据。
CREATE TABLE zhgh_zjnu.bak_sys_unit_before_tree_repair LIKE zhgh_zjnu.sys_unit;
INSERT INTO zhgh_zjnu.bak_sys_unit_before_tree_repair SELECT * FROM zhgh_zjnu.sys_unit;
START TRANSACTION;
-- v3 没有此字段,一级及以上容器取 0,二级及下级部门取 1。
UPDATE zhgh_zjnu.sys_unit
SET unitTypeCode = CASE WHEN unitLevel >= 2 THEN 1 ELSE 0 END
WHERE unitTypeCode IS NULL;
IF NOT EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit WHERE id = '0') THEN
INSERT INTO zhgh_zjnu.sys_unit
(id, parentId, path, name, unitcode, location, hasChildren,
unitLevel, unitTypeCode, delFlag, createdAt, updatedAt)
VALUES
('0', '0', NULL, '组织机构', '0', 0, 0, 0, 0, 0,
CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED),
CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED));
END IF;
UPDATE zhgh_zjnu.sys_unit
SET parentId = '0'
WHERE id <> '0' AND COALESCE(TRIM(parentId), '') IN ('', '0');
SELECT EXISTS (SELECT 1 FROM zhgh_zjnu.sys_unit WHERE parentId = '0' AND id <> '0')
INTO root_has_children;
UPDATE zhgh_zjnu.sys_unit
SET parentId = '0', unitTypeCode = 0, hasChildren = root_has_children
WHERE id = '0';
COMMIT;
END$$
CALL zhgh_zjnu.repair_v4_unit_tree_after_v3_import()$$
DROP PROCEDURE zhgh_zjnu.repair_v4_unit_tree_after_v3_import$$
DELIMITER ;
@@ -0,0 +1,191 @@
-- 仅替换组织机构、组织工会,不修改人员、角色、工会小组及业务数据。
-- 按项目配置:同一 MySQL 8 实例,源库 zhgh_zjnu_v3,目标库 zhgh_zjnu。
-- 保留 v3 的主键和父子关系;v4 独有的机构字段仅在原 ID 相同时保留。
-- 本脚本需独立执行,期间不得有其他连接写入相关数据,不得嵌入已有事务。
-- 若现有人员或关联数据无法继续引用导入的 ID,直接报错,不自动清除关联。
-- 备份表使用固定名称且不覆盖;已存在时拒绝执行,避免丢失首次备份。
DELIMITER $$
DROP PROCEDURE IF EXISTS zhgh_zjnu.replace_v4_unit_union_from_v3$$
CREATE PROCEDURE zhgh_zjnu.replace_v4_unit_union_from_v3()
BEGIN
DECLARE backup_created BOOLEAN DEFAULT FALSE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
DECLARE EXIT HANDLER FOR SQLWARNING
BEGIN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '字段转换产生警告,替换已回滚;请核对源字段长度和类型';
END;
IF (SELECT COUNT(*) FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'zhgh_zjnu' AND TABLE_NAME IN ('sys_unit', 'sys_union')
AND ENGINE = 'InnoDB') <> 2 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '目标机构、工会表必须存在且使用 InnoDB';
END IF;
IF EXISTS (SELECT 1 FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'zhgh_zjnu'
AND TABLE_NAME IN ('bak_sys_unit_before_v3_replace', 'bak_sys_union_before_v3_replace')) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '替换备份表已存在,本次停止,不覆盖原备份';
END IF;
DROP TEMPORARY TABLE IF EXISTS tmp_v3_replace_unit;
DROP TEMPORARY TABLE IF EXISTS tmp_v3_replace_union;
CREATE TEMPORARY TABLE tmp_v3_replace_unit LIKE zhgh_zjnu.sys_unit;
CREATE TEMPORARY TABLE tmp_v3_replace_union LIKE zhgh_zjnu.sys_union;
IF NOT EXISTS (SELECT 1 FROM zhgh_zjnu_v3.sys_unit)
OR NOT EXISTS (SELECT 1 FROM zhgh_zjnu_v3.sys_union) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'v3 机构或工会为空,停止清除 v4 数据';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu_v3.sys_unit
WHERE id IS NULL OR TRIM(id) = '' OR CHAR_LENGTH(id) > 32
OR CHAR_LENGTH(parentId) > 32 OR CHAR_LENGTH(unitcode) > 32) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'v3 机构 ID、父 ID 或编码不符合 v4 长度要求';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu_v3.sys_union
WHERE id IS NULL OR TRIM(id) = '') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'v3 存在空工会 ID,停止替换';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu_v3.sys_unit child
LEFT JOIN zhgh_zjnu_v3.sys_unit parent ON parent.id = child.parentId
WHERE COALESCE(child.parentId, '') NOT IN ('', '0') AND parent.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'v3 机构存在缺失的父机构,停止替换';
END IF;
-- 按目标表结构暂存,唯一键冲突和字段转换异常均发生在清除数据之前。
INSERT INTO tmp_v3_replace_union
(id, name, unionCode, telephone, campus, createdBy, createdAt, updatedBy, updatedAt, delFlag)
SELECT id, unionname, unioncode, telephone, campus, '',
CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED), '',
CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED), 0
FROM zhgh_zjnu_v3.sys_union;
INSERT INTO tmp_v3_replace_unit
(id, parentId, path, name, aliasName, unitcode, note, address, telephone,
email, website, location, hasChildren, logo, unitLevel, unionId, unionGroupId,
createdBy, createdAt, updatedBy, updatedAt, delFlag)
SELECT id, parentId, path, name, aliasName, unitcode, note, address, telephone,
email, website, location, hasChildren, logo, unitlevel, unionid, unionGroupId,
createdBy, createdAt, updatedBy, updatedAt, COALESCE(delFlag, 0)
FROM zhgh_zjnu_v3.sys_unit;
UPDATE tmp_v3_replace_unit incoming
JOIN zhgh_zjnu.sys_unit existing ON existing.id = incoming.id
SET incoming.divisionCollegeCode = existing.divisionCollegeCode,
incoming.institutionType = existing.institutionType,
incoming.unitType = existing.unitType,
incoming.unitTypeCode = existing.unitTypeCode;
IF EXISTS (SELECT 1 FROM tmp_v3_replace_unit unit
LEFT JOIN tmp_v3_replace_union union_info ON union_info.id = unit.unionId
WHERE COALESCE(unit.unionId, '') <> '' AND union_info.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'v3 机构引用的工会不存在,停止替换';
END IF;
-- 人员暂时不动:机构 ID、三级机构 ID、组织路径必须仍然有效。
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_user user_info
LEFT JOIN tmp_v3_replace_unit unit ON unit.id = user_info.unitId
WHERE COALESCE(user_info.unitId, '') <> ''
AND (unit.id IS NULL OR (COALESCE(user_info.unitPath, '') <> ''
AND NOT (user_info.unitPath <=> unit.path)))) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有人员机构 ID 或路径与 v3 不兼容;人员不动时不能直接替换';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_user user_info
LEFT JOIN tmp_v3_replace_unit unit ON unit.id = user_info.threeUnitId
WHERE COALESCE(user_info.threeUnitId, '') <> '' AND unit.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有人员的三级机构不在 v3 中,停止替换';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_user_unit relation_info
LEFT JOIN tmp_v3_replace_unit unit ON unit.id = relation_info.unitId
WHERE COALESCE(relation_info.unitId, '') <> '' AND unit.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有人员兼职机构不在 v3 中,停止替换';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_role role_info
LEFT JOIN tmp_v3_replace_unit unit ON unit.id = role_info.unitid
WHERE COALESCE(role_info.unitid, '') <> '' AND unit.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有角色所属机构不在 v3 中,停止替换';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_user_role relation_info
LEFT JOIN tmp_v3_replace_unit unit ON unit.id = relation_info.unitId
LEFT JOIN tmp_v3_replace_union union_info ON union_info.id = relation_info.unionId
WHERE (COALESCE(relation_info.unitId, '') <> '' AND unit.id IS NULL)
OR (COALESCE(relation_info.unionId, '') <> '' AND union_info.id IS NULL)) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有用户权限范围引用了 v3 中不存在的机构或工会';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_branch_union_user_permission permission_info
LEFT JOIN tmp_v3_replace_union union_info ON union_info.id = permission_info.unionId
WHERE COALESCE(permission_info.unionId, '') <> '' AND union_info.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有分工会人员授权引用的工会不在 v3 中';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.special_staff staff
LEFT JOIN tmp_v3_replace_union union_info ON BINARY union_info.id = BINARY staff.unionRelationUnionId
WHERE COALESCE(staff.unionRelationUnionId, '') <> '' AND union_info.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '特殊人员工会关系引用的工会不在 v3 中';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_union_cadre cadre
LEFT JOIN tmp_v3_replace_union union_info ON union_info.id = cadre.unionId
WHERE COALESCE(cadre.unionId, '') <> '' AND union_info.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有工会干部引用的工会不在 v3 中';
END IF;
-- 工会小组不在本次替换范围,不能写入缺失或归属不一致的小组 ID。
IF EXISTS (SELECT 1 FROM tmp_v3_replace_unit unit
LEFT JOIN zhgh_zjnu.sys_union_group group_info ON group_info.id = unit.unionGroupId
WHERE COALESCE(unit.unionGroupId, '') <> ''
AND (group_info.id IS NULL OR NOT (group_info.unionId <=> unit.unionId))) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'v3 机构的小组关系与 v4 不兼容,需另行明确小组迁移范围';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_union_group group_info
LEFT JOIN tmp_v3_replace_union union_info ON union_info.id = group_info.unionId
WHERE COALESCE(group_info.unionId, '') <> '' AND union_info.id IS NULL) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有工会小组所属工会不在 v3 中';
END IF;
IF EXISTS (SELECT 1 FROM zhgh_zjnu.sys_union_group group_info
JOIN JSON_TABLE(COALESCE(group_info.unitIds, JSON_ARRAY()), '$[*]'
COLUMNS (unitId VARCHAR(100) PATH '$')) binding
LEFT JOIN tmp_v3_replace_unit unit ON BINARY unit.id = BINARY binding.unitId
WHERE unit.id IS NULL OR NOT (unit.unionId <=> group_info.unionId)) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '现有工会小组绑定机构与 v3 不兼容,停止替换';
END IF;
-- DDL 放在事务之前,避免创建备份表导致替换事务被隐式提交。
CREATE TABLE zhgh_zjnu.bak_sys_unit_before_v3_replace LIKE zhgh_zjnu.sys_unit;
CREATE TABLE zhgh_zjnu.bak_sys_union_before_v3_replace LIKE zhgh_zjnu.sys_union;
SET backup_created = TRUE;
START TRANSACTION;
INSERT INTO zhgh_zjnu.bak_sys_unit_before_v3_replace SELECT * FROM zhgh_zjnu.sys_unit;
INSERT INTO zhgh_zjnu.bak_sys_union_before_v3_replace SELECT * FROM zhgh_zjnu.sys_union;
DELETE FROM zhgh_zjnu.sys_unit;
DELETE FROM zhgh_zjnu.sys_union;
INSERT INTO zhgh_zjnu.sys_union SELECT * FROM tmp_v3_replace_union;
INSERT INTO zhgh_zjnu.sys_unit SELECT * FROM tmp_v3_replace_unit;
COMMIT;
DROP TEMPORARY TABLE tmp_v3_replace_unit;
DROP TEMPORARY TABLE tmp_v3_replace_union;
END$$
CALL zhgh_zjnu.replace_v4_unit_union_from_v3()$$
DROP PROCEDURE zhgh_zjnu.replace_v4_unit_union_from_v3$$
DELIMITER ;