Merge remote-tracking branch 'ztcloud/main' into main-ztcloud
# Conflicts: # zt-module-system/zt-module-system-server/src/main/java/com/zt/plat/module/system/service/dept/DeptService.java
This commit is contained in:
@@ -6,6 +6,7 @@ import com.zt.plat.framework.common.util.object.BeanUtils;
|
||||
import com.zt.plat.module.system.api.permission.dto.*;
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleDataScopeReqVO;
|
||||
import com.zt.plat.module.system.controller.admin.permission.vo.permission.PermissionAssignUserRoleReqVO;
|
||||
import com.zt.plat.module.system.enums.permission.DataScopeEnum;
|
||||
import com.zt.plat.module.system.service.permission.PermissionService;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -65,6 +66,11 @@ public class PermissionApiImpl implements PermissionApi {
|
||||
return success(permissionService.getUserRoleIdListByUserIdFromCache(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<DataScopeEnum> getUserDataPermissionLevel(Long userId) {
|
||||
return success(permissionService.getUserDataPermissionLevel(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<Boolean> hasAnyPermissions(Long userId, String... permissions) {
|
||||
return success(permissionService.hasAnyPermissions(userId, permissions));
|
||||
|
||||
@@ -76,4 +76,7 @@ public class DeptSaveReqVO {
|
||||
@Schema(description = "部门来源类型", example = "1")
|
||||
private Integer deptSource;
|
||||
|
||||
@Schema(description = "内部使用:延迟生成部门编码", hidden = true)
|
||||
private Boolean delayCodeGeneration;
|
||||
|
||||
}
|
||||
|
||||
@@ -94,8 +94,9 @@ public class UserController {
|
||||
|
||||
@GetMapping({"/list-all-simple", "/simple-list"})
|
||||
@Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项")
|
||||
public CommonResult<List<UserSimpleRespVO>> getSimpleUserList() {
|
||||
List<AdminUserDO> list = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus(), SIMPLE_LIST_LIMIT);
|
||||
public CommonResult<List<UserSimpleRespVO>> getSimpleUserList(
|
||||
@RequestParam(value = "keyword", required = false) String keyword) {
|
||||
List<AdminUserDO> list = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus(), SIMPLE_LIST_LIMIT, keyword);
|
||||
return success(UserConvert.INSTANCE.convertSimpleList(list));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ public class UserPageReqVO extends PageParam {
|
||||
@Schema(description = "用户账号,模糊匹配", example = "zt")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "用户昵称,模糊匹配", example = "张三")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "工号,模糊匹配", example = "A00123")
|
||||
private String workcode;
|
||||
|
||||
|
||||
@@ -114,12 +114,15 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
|
||||
* @param parentId 父部门ID
|
||||
* @return 编码最大的子部门
|
||||
*/
|
||||
default DeptDO selectLastChildByCode(Long parentId) {
|
||||
return selectOne(new LambdaQueryWrapper<DeptDO>()
|
||||
default DeptDO selectLastChildByCode(Long parentId, String prefix) {
|
||||
LambdaQueryWrapper<DeptDO> wrapper = new LambdaQueryWrapper<DeptDO>()
|
||||
.eq(DeptDO::getParentId, parentId)
|
||||
.isNotNull(DeptDO::getCode)
|
||||
.orderByDesc(DeptDO::getCode)
|
||||
.last("LIMIT 1"));
|
||||
.isNotNull(DeptDO::getCode);
|
||||
if (StrUtil.isNotBlank(prefix)) {
|
||||
wrapper.likeRight(DeptDO::getCode, prefix);
|
||||
}
|
||||
wrapper.orderByDesc(DeptDO::getCode).last("LIMIT 1");
|
||||
return selectOne(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,6 +40,7 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
|
||||
MPJLambdaWrapperX<AdminUserDO> query = new MPJLambdaWrapperX<>();
|
||||
query.leftJoin(UserDeptDO.class, UserDeptDO::getUserId, AdminUserDO::getId);
|
||||
query.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername());
|
||||
query.likeIfPresent(AdminUserDO::getNickname, reqVO.getNickname());
|
||||
query.likeIfPresent(AdminUserDO::getWorkcode, reqVO.getWorkcode());
|
||||
query.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile());
|
||||
query.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus());
|
||||
@@ -70,9 +71,16 @@ public interface AdminUserMapper extends BaseMapperX<AdminUserDO> {
|
||||
return selectList(new LambdaQueryWrapperX<AdminUserDO>().like(AdminUserDO::getNickname, nickname));
|
||||
}
|
||||
|
||||
default List<AdminUserDO> selectListByStatus(Integer status, Integer limit) {
|
||||
default List<AdminUserDO> selectListByStatus(Integer status, Integer limit, String keyword) {
|
||||
LambdaQueryWrapperX<AdminUserDO> query = new LambdaQueryWrapperX<AdminUserDO>()
|
||||
.eq(AdminUserDO::getStatus, status);
|
||||
if (StrUtil.isNotBlank(keyword)) {
|
||||
String trimmed = keyword.trim();
|
||||
query.and(w -> w.like(AdminUserDO::getNickname, trimmed)
|
||||
.or().like(AdminUserDO::getUsername, trimmed)
|
||||
.or().like(AdminUserDO::getMobile, trimmed)
|
||||
.or().like(AdminUserDO::getWorkcode, trimmed));
|
||||
}
|
||||
if (limit != null && limit > 0) {
|
||||
query.last("LIMIT " + limit);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
|
||||
import com.zt.plat.framework.common.enums.CommonStatusEnum;
|
||||
import com.zt.plat.framework.common.pojo.CompanyDeptInfo;
|
||||
import com.zt.plat.framework.common.util.object.BeanUtils;
|
||||
@@ -11,18 +12,14 @@ import com.zt.plat.framework.datapermission.core.annotation.DataPermission;
|
||||
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
|
||||
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptListReqVO;
|
||||
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
|
||||
import com.zt.plat.module.system.controller.admin.dict.vo.data.DictDataSaveReqVO;
|
||||
import com.zt.plat.module.system.controller.admin.dict.vo.type.DictTypeSaveReqVO;
|
||||
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
|
||||
import com.zt.plat.module.system.dal.dataobject.dict.DictTypeDO;
|
||||
import com.zt.plat.module.system.dal.dataobject.userdept.UserDeptDO;
|
||||
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
|
||||
import com.zt.plat.module.system.dal.mysql.userdept.UserDeptMapper;
|
||||
import com.zt.plat.module.system.service.dept.DeptExternalCodeService;
|
||||
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
|
||||
import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
|
||||
import com.zt.plat.module.system.enums.DictTypeConstants;
|
||||
import com.zt.plat.module.system.service.dict.DictDataService;
|
||||
import com.zt.plat.module.system.service.dict.DictTypeService;
|
||||
import com.zt.plat.module.system.service.permission.PermissionService;
|
||||
import org.apache.seata.spring.annotation.GlobalTransactional;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -57,17 +54,17 @@ public class DeptServiceImpl implements DeptService {
|
||||
@Resource
|
||||
private UserDeptMapper userDeptMapper;
|
||||
@Resource
|
||||
private PermissionService permissionService;
|
||||
@Resource
|
||||
private com.zt.plat.module.system.mq.producer.databus.DatabusChangeProducer databusChangeProducer;
|
||||
@Resource
|
||||
private DeptExternalCodeService deptExternalCodeService;
|
||||
@Resource
|
||||
private DictTypeService dictTypeService;
|
||||
@Resource
|
||||
private DictDataService dictDataService;
|
||||
|
||||
private static final String ROOT_CODE_PREFIX = "ZT";
|
||||
private static final String EXTERNAL_CODE_PREFIX = "CU";
|
||||
private static final int CODE_SEGMENT_LENGTH = 3;
|
||||
private static final int MAX_SEQUENCE = 999;
|
||||
private static final int BATCH_SIZE = 1000;
|
||||
private static final Comparator<DeptDO> DEPT_COMPARATOR = Comparator
|
||||
.comparing(DeptDO::getSort, Comparator.nullsLast(Comparator.naturalOrder()))
|
||||
.thenComparing(DeptDO::getId, Comparator.nullsLast(Comparator.naturalOrder()));
|
||||
@@ -82,26 +79,33 @@ public class DeptServiceImpl implements DeptService {
|
||||
createReqVO.setParentId(normalizeParentId(createReqVO.getParentId()));
|
||||
// 创建时默认有效
|
||||
createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
// 默认部门来源:未指定时视为外部部门
|
||||
if (createReqVO.getDeptSource() == null) {
|
||||
createReqVO.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
|
||||
}
|
||||
// 校验父部门的有效性
|
||||
validateParentDept(null, createReqVO.getParentId());
|
||||
// 校验部门名的唯一性
|
||||
validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
|
||||
// 生成并校验部门编码
|
||||
Long effectiveParentId = normalizeParentId(createReqVO.getParentId());
|
||||
String resolvedCode = generateDeptCode(effectiveParentId);
|
||||
validateDeptCodeUnique(null, resolvedCode);
|
||||
createReqVO.setCode(resolvedCode);
|
||||
// 生成并校验部门编码(所有来源统一走生成逻辑,iWork 不再豁免)
|
||||
if (Boolean.TRUE.equals(createReqVO.getDelayCodeGeneration())) {
|
||||
createReqVO.setCode(null);
|
||||
} else {
|
||||
String resolvedCode = generateDeptCode(createReqVO.getParentId(), createReqVO.getDeptSource());
|
||||
validateDeptCodeUnique(null, resolvedCode);
|
||||
createReqVO.setCode(resolvedCode);
|
||||
}
|
||||
|
||||
// 插入部门
|
||||
DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class);
|
||||
// 设置部门来源:如果未指定,默认为外部部门
|
||||
// 设置部门来源(前置已默认化,此处兜底)
|
||||
if (dept.getDeptSource() == null) {
|
||||
dept.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
|
||||
}
|
||||
deptMapper.insert(dept);
|
||||
|
||||
// 维护外部系统编码映射(若有传入)
|
||||
upsertExternalCodeMapping(createReqVO, dept.getId());
|
||||
// 外部编码映射
|
||||
upsertExternalMappingIfPresent(dept.getId(), createReqVO);
|
||||
|
||||
// 发布部门创建事件
|
||||
databusChangeProducer.sendDeptCreatedMessage(dept);
|
||||
@@ -109,6 +113,15 @@ public class DeptServiceImpl implements DeptService {
|
||||
return dept.getId();
|
||||
}
|
||||
|
||||
private void upsertExternalMappingIfPresent(Long deptId, DeptSaveReqVO reqVO) {
|
||||
String systemCode = StrUtil.trimToNull(reqVO.getExternalSystemCode());
|
||||
String externalCode = StrUtil.trimToNull(reqVO.getExternalDeptCode());
|
||||
if (StrUtil.hasEmpty(systemCode, externalCode) || deptId == null) {
|
||||
return;
|
||||
}
|
||||
String externalName = StrUtil.trimToNull(reqVO.getExternalDeptName());
|
||||
deptExternalCodeService.saveOrUpdateDeptExternalCode(deptId, systemCode, externalCode, externalName, reqVO.getStatus());
|
||||
}
|
||||
@Override
|
||||
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
|
||||
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
|
||||
@@ -122,34 +135,36 @@ public class DeptServiceImpl implements DeptService {
|
||||
validateParentDept(updateReqVO.getId(), updateReqVO.getParentId());
|
||||
// 校验部门名的唯一性
|
||||
validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
|
||||
Long newParentId = normalizeParentId(updateReqVO.getParentId());
|
||||
Long oldParentId = normalizeParentId(originalDept.getParentId());
|
||||
boolean parentChanged = !Objects.equals(newParentId, oldParentId);
|
||||
String existingCode = originalDept.getCode();
|
||||
boolean needRegenerateCode = StrUtil.isBlank(existingCode);
|
||||
String resolvedCode = existingCode;
|
||||
if (needRegenerateCode) {
|
||||
resolvedCode = generateDeptCode(newParentId);
|
||||
validateDeptCodeUnique(updateReqVO.getId(), resolvedCode);
|
||||
Integer source = ObjectUtil.defaultIfNull(updateReqVO.getDeptSource(), originalDept.getDeptSource());
|
||||
if (source == null) {
|
||||
source = DeptSourceEnum.EXTERNAL.getSource();
|
||||
}
|
||||
String existingCode = originalDept.getCode();
|
||||
if (StrUtil.isBlank(existingCode)) {
|
||||
if (Boolean.TRUE.equals(updateReqVO.getDelayCodeGeneration())) {
|
||||
updateReqVO.setCode(null);
|
||||
} else {
|
||||
String newCode = generateDeptCode(updateReqVO.getParentId(), source);
|
||||
validateDeptCodeUnique(updateReqVO.getId(), newCode);
|
||||
updateReqVO.setCode(newCode);
|
||||
}
|
||||
} else {
|
||||
updateReqVO.setCode(existingCode);
|
||||
}
|
||||
updateReqVO.setCode(resolvedCode);
|
||||
|
||||
// 更新部门
|
||||
DeptDO updateObj = BeanUtils.toBean(updateReqVO, DeptDO.class);
|
||||
deptMapper.updateById(updateObj);
|
||||
|
||||
// 外部编码映射
|
||||
upsertExternalMappingIfPresent(updateObj.getId(), updateReqVO);
|
||||
|
||||
// 发布部门更新事件(重新查询获取完整数据)
|
||||
DeptDO updatedDept = deptMapper.selectById(updateObj.getId());
|
||||
if (updatedDept != null) {
|
||||
databusChangeProducer.sendDeptUpdatedMessage(updatedDept);
|
||||
}
|
||||
|
||||
if (needRegenerateCode) {
|
||||
refreshChildCodesRecursively(updateObj.getId(), updateReqVO.getCode());
|
||||
}
|
||||
|
||||
// 维护外部系统编码映射(若有传入)
|
||||
upsertExternalCodeMapping(updateReqVO, updateReqVO.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,7 +182,7 @@ public class DeptServiceImpl implements DeptService {
|
||||
DeptDO dept = deptMapper.selectById(id);
|
||||
Long tenantId = (dept != null) ? dept.getTenantId() : null;
|
||||
|
||||
// 级联删除外部编码映射并清理缓存
|
||||
// 级联删除外部编码映射
|
||||
deptExternalCodeService.deleteDeptExternalCodesByDeptId(id);
|
||||
|
||||
// 删除部门
|
||||
@@ -268,26 +283,16 @@ public class DeptServiceImpl implements DeptService {
|
||||
}
|
||||
}
|
||||
|
||||
private String generateDeptCode(Long parentId) {
|
||||
private String generateDeptCode(Long parentId, Integer deptSource) {
|
||||
Long effectiveParentId = normalizeParentId(parentId);
|
||||
Long codeParentId = effectiveParentId;
|
||||
String prefix = ROOT_CODE_PREFIX;
|
||||
if (!DeptDO.PARENT_ID_ROOT.equals(effectiveParentId)) {
|
||||
DeptDO parentDept = deptMapper.selectById(effectiveParentId);
|
||||
if (parentDept == null || StrUtil.isBlank(parentDept.getCode())) {
|
||||
codeParentId = DeptDO.PARENT_ID_ROOT;
|
||||
} else {
|
||||
prefix = parentDept.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
int nextSequence = determineNextSequence(codeParentId, prefix);
|
||||
String prefix = resolveCodePrefix(effectiveParentId, deptSource);
|
||||
int nextSequence = determineNextSequence(effectiveParentId, prefix);
|
||||
assertSequenceRange(nextSequence);
|
||||
return prefix + formatSequence(nextSequence);
|
||||
}
|
||||
|
||||
private int determineNextSequence(Long parentId, String prefix) {
|
||||
DeptDO lastChild = deptMapper.selectLastChildByCode(parentId);
|
||||
DeptDO lastChild = deptMapper.selectLastChildByCode(parentId, prefix);
|
||||
Integer sequence = parseSequence(lastChild != null ? lastChild.getCode() : null, prefix);
|
||||
if (sequence != null) {
|
||||
return sequence + 1;
|
||||
@@ -365,12 +370,36 @@ public class DeptServiceImpl implements DeptService {
|
||||
candidate = candidate.trim();
|
||||
}
|
||||
if (StrUtil.isBlank(candidate)) {
|
||||
candidate = generateDeptCode(DeptDO.PARENT_ID_ROOT);
|
||||
candidate = generateDeptCode(DeptDO.PARENT_ID_ROOT, DeptSourceEnum.EXTERNAL.getSource());
|
||||
}
|
||||
validateDeptCodeUnique(currentDeptId, candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String resolveCodePrefix(Long parentId, Integer deptSource) {
|
||||
boolean isExternal = Objects.equals(deptSource, DeptSourceEnum.EXTERNAL.getSource());
|
||||
if (DeptDO.PARENT_ID_ROOT.equals(parentId)) {
|
||||
return isExternal ? EXTERNAL_CODE_PREFIX : ROOT_CODE_PREFIX;
|
||||
}
|
||||
|
||||
DeptDO parentDept = deptMapper.selectById(parentId);
|
||||
if (parentDept == null || StrUtil.isBlank(parentDept.getCode())) {
|
||||
return isExternal ? EXTERNAL_CODE_PREFIX : ROOT_CODE_PREFIX;
|
||||
}
|
||||
|
||||
String parentCode = parentDept.getCode();
|
||||
if (isExternal) {
|
||||
if (parentCode.startsWith(EXTERNAL_CODE_PREFIX)) {
|
||||
return parentCode;
|
||||
}
|
||||
if (parentCode.startsWith(ROOT_CODE_PREFIX)) {
|
||||
return EXTERNAL_CODE_PREFIX + parentCode.substring(ROOT_CODE_PREFIX.length());
|
||||
}
|
||||
return EXTERNAL_CODE_PREFIX;
|
||||
}
|
||||
return parentCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeptDO getDept(Long id) {
|
||||
return deptMapper.selectById(id);
|
||||
@@ -558,37 +587,59 @@ public class DeptServiceImpl implements DeptService {
|
||||
|
||||
@Override
|
||||
public List<DeptDO> getTopLevelDeptList() {
|
||||
// 获取当前用户所属的部门列表
|
||||
Set<Long> deptIds = userDeptMapper.selectValidListByUserIds(singleton(getLoginUserId()))
|
||||
.stream()
|
||||
.map(UserDeptDO::getDeptId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Long loginUserId = getLoginUserId();
|
||||
|
||||
// 当前用户所属部门
|
||||
Set<Long> userDeptIds = Optional.ofNullable(userDeptMapper.selectValidListByUserIds(singleton(loginUserId)))
|
||||
.orElseGet(Collections::emptyList)
|
||||
.stream()
|
||||
.map(UserDeptDO::getDeptId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 数据权限部门
|
||||
DeptDataPermissionRespDTO dataPerm = permissionService.getDeptDataPermission(loginUserId);
|
||||
Set<Long> permDeptIds = Optional.ofNullable(dataPerm)
|
||||
.map(DeptDataPermissionRespDTO::getDeptIds)
|
||||
.orElse(Collections.emptySet());
|
||||
|
||||
// all=true 直接返回根级启用部门
|
||||
if (dataPerm != null && Boolean.TRUE.equals(dataPerm.getAll())) {
|
||||
List<DeptDO> roots = deptMapper.selectListByParentId(DeptDO.PARENT_ID_ROOT, CommonStatusEnum.ENABLE.getStatus());
|
||||
roots.sort(DEPT_COMPARATOR);
|
||||
return roots;
|
||||
}
|
||||
|
||||
// 合并两类部门 ID,仅在并集为空时返回空
|
||||
Set<Long> deptIds = new HashSet<>();
|
||||
deptIds.addAll(userDeptIds);
|
||||
deptIds.addAll(Optional.ofNullable(permDeptIds).orElse(Collections.emptySet()));
|
||||
|
||||
if (CollUtil.isEmpty(deptIds)) {
|
||||
// 如果用户没有关联任何部门,返回空列表
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 获取用户所属部门的最顶层祖先部门
|
||||
Set<Long> topLevelDeptIds = new HashSet<>();
|
||||
for (Long deptId : deptIds) {
|
||||
DeptDO dept = getDept(deptId);
|
||||
if (dept != null && CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) {
|
||||
// 找到该部门的最顶层祖先
|
||||
DeptDO topLevelDept = findTopLevelAncestor(dept);
|
||||
if (topLevelDept != null) {
|
||||
topLevelDeptIds.add(topLevelDept.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据顶层部门ID获取部门详情
|
||||
return topLevelDeptIds.stream()
|
||||
.map(this::getDept)
|
||||
.filter(Objects::nonNull)
|
||||
.filter(dept -> CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus()))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 缓存已加载的部门,避免重复 IO
|
||||
Map<Long, DeptDO> deptCache = new HashMap<>();
|
||||
|
||||
// 批量解析最顶层祖先(到 ROOT 或上级禁用即停),减少循环 IO
|
||||
Map<Long, Long> topLevelMap = findTopLevelAncestorIdsBatch(deptIds, deptCache);
|
||||
|
||||
// 汇总顶层部门 ID 并取实体(使用缓存避免再查)
|
||||
Set<Long> topLevelDeptIds = topLevelMap.values().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
List<DeptDO> topLevelDepts = topLevelDeptIds.stream()
|
||||
.map(id -> deptCache.computeIfAbsent(id, this::getDept))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(dept -> CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus()))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 按 sort(nullsLast)再按 id 排序
|
||||
topLevelDepts.sort(DEPT_COMPARATOR);
|
||||
return topLevelDepts;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -741,64 +792,134 @@ public class DeptServiceImpl implements DeptService {
|
||||
return dept;
|
||||
}
|
||||
|
||||
private void upsertExternalCodeMapping(DeptSaveReqVO reqVO, Long deptId) {
|
||||
if (reqVO == null || deptId == null) {
|
||||
return;
|
||||
/**
|
||||
* 批量查找部门的最顶层祖先(到 ROOT 或遇到禁用/缺失的父部门即停止)
|
||||
* 使用 1000 条分片批量查询,减少循环 IO
|
||||
*
|
||||
* @param deptIds 待解析的部门 ID 集合
|
||||
* @param deptCache 部门缓存(可复用外部缓存)
|
||||
* @return 原始部门 ID -> 顶层祖先部门 ID 映射(若未找到则为 null)
|
||||
*/
|
||||
private Map<Long, Long> findTopLevelAncestorIdsBatch(Set<Long> deptIds, Map<Long, DeptDO> deptCache) {
|
||||
Map<Long, Long> result = new HashMap<>();
|
||||
if (CollUtil.isEmpty(deptIds)) {
|
||||
return result;
|
||||
}
|
||||
String systemCode = StrUtil.trimToNull(reqVO.getExternalSystemCode());
|
||||
String externalCode = StrUtil.trimToNull(reqVO.getExternalDeptCode());
|
||||
if (StrUtil.isBlank(systemCode) || StrUtil.isBlank(externalCode)) {
|
||||
return;
|
||||
|
||||
// 当前指针:原始部门 -> 当前向上追溯的部门 ID
|
||||
Map<Long, Long> cursorMap = new HashMap<>();
|
||||
for (Long id : deptIds) {
|
||||
cursorMap.put(id, id);
|
||||
}
|
||||
// 缺失的外部系统字典类型或数据会自动补齐
|
||||
ensureExternalSystemDict(systemCode);
|
||||
deptExternalCodeService.saveOrUpdateDeptExternalCode(
|
||||
deptId,
|
||||
systemCode,
|
||||
externalCode,
|
||||
reqVO.getExternalDeptName(),
|
||||
CommonStatusEnum.ENABLE.getStatus());
|
||||
|
||||
// 预先加载首批部门
|
||||
loadDeptBatch(cursorMap.values(), deptCache);
|
||||
|
||||
int safety = 0;
|
||||
while (!cursorMap.isEmpty() && safety++ < Short.MAX_VALUE) {
|
||||
// 收集本轮需要加载的父部门 ID(避免重复加载)
|
||||
Set<Long> parentIdsToLoad = new HashSet<>();
|
||||
for (Long currentId : cursorMap.values()) {
|
||||
DeptDO current = deptCache.get(currentId);
|
||||
if (current == null) {
|
||||
continue;
|
||||
}
|
||||
Long parentId = current.getParentId();
|
||||
if (parentId != null && !DeptDO.PARENT_ID_ROOT.equals(parentId) && !deptCache.containsKey(parentId)) {
|
||||
parentIdsToLoad.add(parentId);
|
||||
}
|
||||
}
|
||||
loadDeptBatch(parentIdsToLoad, deptCache);
|
||||
|
||||
// 遍历当前指针,决定是否上卷或结束
|
||||
Iterator<Map.Entry<Long, Long>> iterator = cursorMap.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<Long, Long> entry = iterator.next();
|
||||
Long originalId = entry.getKey();
|
||||
Long currentId = entry.getValue();
|
||||
DeptDO current = deptCache.get(currentId);
|
||||
|
||||
if (current == null) {
|
||||
result.put(originalId, null);
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
Long parentId = current.getParentId();
|
||||
if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) {
|
||||
// 已到达 ROOT(顶层)
|
||||
result.put(originalId, current.getId());
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
DeptDO parent = deptCache.get(parentId);
|
||||
if (parent == null || !CommonStatusEnum.ENABLE.getStatus().equals(parent.getStatus())) {
|
||||
// 父部门缺失或禁用,则当前部门视为顶层
|
||||
result.put(originalId, current.getId());
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 向上继续追溯
|
||||
entry.setValue(parentId);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保外部系统字典存在(含字典类型与对应值),若缺失则自动创建
|
||||
* 将给定的部门 ID 集合按批次加载到缓存
|
||||
*/
|
||||
private void ensureExternalSystemDict(String systemCode) {
|
||||
String normalizedCode = StrUtil.trimToNull(systemCode);
|
||||
if (normalizedCode == null) {
|
||||
private void loadDeptBatch(Collection<Long> ids, Map<Long, DeptDO> deptCache) {
|
||||
if (CollUtil.isEmpty(ids)) {
|
||||
return;
|
||||
}
|
||||
List<Long> toLoad = ids.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(id -> !deptCache.containsKey(id))
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (CollUtil.isEmpty(toLoad)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
DictTypeDO dictType = dictTypeService.getDictType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM);
|
||||
if (dictType == null) {
|
||||
DictTypeSaveReqVO typeReq = new DictTypeSaveReqVO();
|
||||
typeReq.setName("部门外部系统标识");
|
||||
typeReq.setType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM);
|
||||
typeReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
typeReq.setRemark("外部组织同步自动创建");
|
||||
dictTypeService.createDictType(typeReq);
|
||||
} else if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) {
|
||||
DictTypeSaveReqVO updateReq = new DictTypeSaveReqVO();
|
||||
updateReq.setId(dictType.getId());
|
||||
updateReq.setName(dictType.getName());
|
||||
updateReq.setType(dictType.getType());
|
||||
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
updateReq.setRemark(dictType.getRemark());
|
||||
dictTypeService.updateDictType(updateReq);
|
||||
}
|
||||
|
||||
if (dictDataService.getDictData(DictTypeConstants.DEPT_EXTERNAL_SYSTEM, normalizedCode) == null) {
|
||||
DictDataSaveReqVO dataReq = new DictDataSaveReqVO();
|
||||
dataReq.setDictType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM);
|
||||
dataReq.setLabel(normalizedCode);
|
||||
dataReq.setValue(normalizedCode);
|
||||
dataReq.setSort(0);
|
||||
dataReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
dataReq.setRemark("外部组织同步自动创建");
|
||||
dictDataService.createDictData(dataReq);
|
||||
for (int i = 0; i < toLoad.size(); i += BATCH_SIZE) {
|
||||
int end = Math.min(i + BATCH_SIZE, toLoad.size());
|
||||
List<Long> batch = toLoad.subList(i, end);
|
||||
List<DeptDO> depts = getDeptList(batch);
|
||||
if (CollUtil.isEmpty(depts)) {
|
||||
continue;
|
||||
}
|
||||
for (DeptDO dept : depts) {
|
||||
if (dept != null && dept.getId() != null) {
|
||||
deptCache.putIfAbsent(dept.getId(), dept);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DataPermission(enable = false)
|
||||
public void backfillMissingCodesWithoutEvent(Collection<Long> deptIds) {
|
||||
if (CollUtil.isEmpty(deptIds)) {
|
||||
return;
|
||||
}
|
||||
List<DeptDO> targets = deptMapper.selectBatchIds(deptIds);
|
||||
for (DeptDO dept : targets) {
|
||||
if (dept == null || StrUtil.isNotBlank(dept.getCode())) {
|
||||
continue;
|
||||
}
|
||||
Integer source = ObjectUtil.defaultIfNull(dept.getDeptSource(), DeptSourceEnum.EXTERNAL.getSource());
|
||||
try {
|
||||
String code = generateDeptCode(dept.getParentId(), source);
|
||||
validateDeptCodeUnique(dept.getId(), code);
|
||||
updateDeptCode(dept.getId(), code);
|
||||
} catch (Exception ex) {
|
||||
log.warn("[iWork] 回填部门编码失败 id={} name={} msg={}", dept.getId(), dept.getName(), ex.getMessage());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("[Dept] Ensure external system dict failed, systemCode={}", normalizedCode, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,23 +5,42 @@ import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrJo
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Abstraction for applying iWork entities into local persistence.
|
||||
*/
|
||||
public interface IWorkSyncProcessor {
|
||||
|
||||
BatchResult syncSubcompanies(List<IWorkHrSubcompanyPageRespVO.Subcompany> data, SyncOptions options);
|
||||
BatchResult syncSubcompanies(List<IWorkHrSubcompanyPageRespVO.Subcompany> data,
|
||||
SyncOptions options);
|
||||
|
||||
BatchResult syncSubcompanies(List<IWorkHrSubcompanyPageRespVO.Subcompany> data,
|
||||
SyncOptions options,
|
||||
DeptSyncContext context);
|
||||
|
||||
BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data, SyncOptions options);
|
||||
|
||||
BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data,
|
||||
SyncOptions options,
|
||||
DeptSyncContext context);
|
||||
|
||||
BatchResult syncJobTitles(List<IWorkHrJobTitlePageRespVO.JobTitle> data, SyncOptions options);
|
||||
|
||||
BatchResult syncUsers(List<IWorkHrUserPageRespVO.User> data, SyncOptions options);
|
||||
|
||||
/**
|
||||
* 对当次同步累计的待处理/占位部门做最终补偿(跨页父子依赖)。
|
||||
*/
|
||||
default BatchResult flushDeptPending(DeptSyncContext context, SyncOptions options) {
|
||||
return BatchResult.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution options shared by batch and single sync flows.
|
||||
*/
|
||||
@@ -53,6 +72,32 @@ public interface IWorkSyncProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 部门/分部跨页同步上下文,用于累计待处理记录与已就绪父级。
|
||||
*/
|
||||
final class DeptSyncContext {
|
||||
private final Set<Long> readyParentIds = new HashSet<>();
|
||||
private final List<IWorkHrSubcompanyPageRespVO.Subcompany> pendingSubcompanies = new ArrayList<>();
|
||||
private final List<IWorkHrDepartmentPageRespVO.Department> pendingDepartments = new ArrayList<>();
|
||||
private final Set<Long> placeholderDeptIds = new HashSet<>();
|
||||
|
||||
public Set<Long> getReadyParentIds() {
|
||||
return readyParentIds;
|
||||
}
|
||||
|
||||
public List<IWorkHrSubcompanyPageRespVO.Subcompany> getPendingSubcompanies() {
|
||||
return pendingSubcompanies;
|
||||
}
|
||||
|
||||
public List<IWorkHrDepartmentPageRespVO.Department> getPendingDepartments() {
|
||||
return pendingDepartments;
|
||||
}
|
||||
|
||||
public Set<Long> getPlaceholderDeptIds() {
|
||||
return placeholderDeptIds;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregated result for a sync batch.
|
||||
*/
|
||||
@@ -170,11 +215,11 @@ public interface IWorkSyncProcessor {
|
||||
}
|
||||
|
||||
default BatchResult syncSubcompany(IWorkHrSubcompanyPageRespVO.Subcompany data, SyncOptions options) {
|
||||
return syncSubcompanies(Collections.singletonList(data), options);
|
||||
return syncSubcompanies(Collections.singletonList(data), options, null);
|
||||
}
|
||||
|
||||
default BatchResult syncDepartment(IWorkHrDepartmentPageRespVO.Department data, SyncOptions options) {
|
||||
return syncDepartments(Collections.singletonList(data), options);
|
||||
return syncDepartments(Collections.singletonList(data), options, null);
|
||||
}
|
||||
|
||||
default BatchResult syncJobTitle(IWorkHrJobTitlePageRespVO.JobTitle data, SyncOptions options) {
|
||||
|
||||
@@ -50,13 +50,34 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
|
||||
@Override
|
||||
public BatchResult syncSubcompanies(List<IWorkHrSubcompanyPageRespVO.Subcompany> data, SyncOptions options) {
|
||||
return syncSubcompanies(data, options, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchResult syncSubcompanies(List<IWorkHrSubcompanyPageRespVO.Subcompany> data,
|
||||
SyncOptions options,
|
||||
DeptSyncContext context) {
|
||||
return syncSubcompaniesInternal(data, options, context, false);
|
||||
}
|
||||
|
||||
private BatchResult syncSubcompaniesInternal(List<IWorkHrSubcompanyPageRespVO.Subcompany> data,
|
||||
SyncOptions options,
|
||||
DeptSyncContext context,
|
||||
boolean allowPlaceholderOnRemaining) {
|
||||
List<IWorkHrSubcompanyPageRespVO.Subcompany> records = CollUtil.emptyIfNull(data);
|
||||
BatchResult result = BatchResult.empty();
|
||||
if (records.isEmpty()) {
|
||||
if (records.isEmpty()
|
||||
&& (context == null || CollUtil.isEmpty(context.getPendingSubcompanies()))) {
|
||||
return result;
|
||||
}
|
||||
result.increasePulled(records.size());
|
||||
List<IWorkHrSubcompanyPageRespVO.Subcompany> queue = new ArrayList<>(records);
|
||||
List<IWorkHrSubcompanyPageRespVO.Subcompany> queue = new ArrayList<>();
|
||||
if (context != null && CollUtil.isNotEmpty(context.getPendingSubcompanies())) {
|
||||
queue.addAll(context.getPendingSubcompanies());
|
||||
context.getPendingSubcompanies().clear();
|
||||
}
|
||||
queue.addAll(records);
|
||||
Set<Long> readyParentIds = context != null ? context.getReadyParentIds() : new HashSet<>();
|
||||
int guard = 0;
|
||||
int maxPasses = Math.max(1, queue.size() * 2);
|
||||
while (!queue.isEmpty() && guard++ < maxPasses) {
|
||||
@@ -79,6 +100,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
}
|
||||
Long deptId = externalId.longValue();
|
||||
ParentHolder parentHolder = resolveSubcompanyParent(sub.getSupsubcomid());
|
||||
if (!isParentReady(parentHolder.parentId(), readyParentIds)) {
|
||||
continue;
|
||||
}
|
||||
boolean canceled = isCanceledFlag(sub.getCanceled());
|
||||
DeptSaveReqVO saveReq = buildSubcompanySaveReq(sub, deptId, parentHolder.parentId(), canceled);
|
||||
try {
|
||||
@@ -87,6 +111,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
canceled,
|
||||
options);
|
||||
applyDeptOutcome(result, outcome, "分部", sub.getSubcompanyname());
|
||||
if (outcome.deptId() != null) {
|
||||
readyParentIds.add(outcome.deptId());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("[iWork] 同步分部失败: id={} name={}", sub.getId(), sub.getSubcompanyname(), ex);
|
||||
result.increaseFailed();
|
||||
@@ -99,10 +126,30 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!queue.isEmpty()) {
|
||||
if (context != null && !allowPlaceholderOnRemaining) {
|
||||
context.getPendingSubcompanies().addAll(queue);
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
if (!queue.isEmpty()) {
|
||||
for (IWorkHrSubcompanyPageRespVO.Subcompany remaining : queue) {
|
||||
log.warn("[iWork] 分部因父级缺失未同步: id={} name={}", remaining.getId(), remaining.getSubcompanyname());
|
||||
result.increaseFailed();
|
||||
log.warn("[iWork] 分部父级缺失,延迟生成编码插入占位: id={} name={}", remaining.getId(), remaining.getSubcompanyname());
|
||||
DeptSaveReqVO saveReq = buildSubcompanySaveReq(remaining,
|
||||
remaining.getId() == null ? null : remaining.getId().longValue(),
|
||||
resolveSubcompanyParent(remaining.getSupsubcomid()).parentId(),
|
||||
isCanceledFlag(remaining.getCanceled()));
|
||||
saveReq.setDelayCodeGeneration(true);
|
||||
try {
|
||||
DeptSyncOutcome outcome = upsertDept(saveReq.getId(), saveReq, isCanceledFlag(remaining.getCanceled()), options);
|
||||
applyDeptOutcome(result, outcome, "分部", remaining.getSubcompanyname());
|
||||
if (context != null && outcome.deptId() != null) {
|
||||
context.getPlaceholderDeptIds().add(outcome.deptId());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("[iWork] 分部占位插入失败: id={} name={}", remaining.getId(), remaining.getSubcompanyname(), ex);
|
||||
result.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -110,13 +157,34 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
|
||||
@Override
|
||||
public BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data, SyncOptions options) {
|
||||
return syncDepartments(data, options, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data,
|
||||
SyncOptions options,
|
||||
DeptSyncContext context) {
|
||||
return syncDepartmentsInternal(data, options, context, false);
|
||||
}
|
||||
|
||||
private BatchResult syncDepartmentsInternal(List<IWorkHrDepartmentPageRespVO.Department> data,
|
||||
SyncOptions options,
|
||||
DeptSyncContext context,
|
||||
boolean allowPlaceholderOnRemaining) {
|
||||
List<IWorkHrDepartmentPageRespVO.Department> records = CollUtil.emptyIfNull(data);
|
||||
BatchResult result = BatchResult.empty();
|
||||
if (records.isEmpty()) {
|
||||
if (records.isEmpty()
|
||||
&& (context == null || CollUtil.isEmpty(context.getPendingDepartments()))) {
|
||||
return result;
|
||||
}
|
||||
result.increasePulled(records.size());
|
||||
List<IWorkHrDepartmentPageRespVO.Department> queue = new ArrayList<>(records);
|
||||
List<IWorkHrDepartmentPageRespVO.Department> queue = new ArrayList<>();
|
||||
if (context != null && CollUtil.isNotEmpty(context.getPendingDepartments())) {
|
||||
queue.addAll(context.getPendingDepartments());
|
||||
context.getPendingDepartments().clear();
|
||||
}
|
||||
queue.addAll(records);
|
||||
Set<Long> readyParentIds = context != null ? context.getReadyParentIds() : new HashSet<>();
|
||||
int guard = 0;
|
||||
int maxPasses = Math.max(1, queue.size() * 2);
|
||||
while (!queue.isEmpty() && guard++ < maxPasses) {
|
||||
@@ -139,6 +207,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
}
|
||||
Long deptId = externalId.longValue();
|
||||
ParentHolder parentHolder = resolveDepartmentParent(dept);
|
||||
if (!isParentReady(parentHolder.parentId(), readyParentIds)) {
|
||||
continue;
|
||||
}
|
||||
boolean canceled = isCanceledFlag(dept.getCanceled());
|
||||
DeptSaveReqVO saveReq = buildDepartmentSaveReq(dept, deptId, parentHolder.parentId(), canceled);
|
||||
try {
|
||||
@@ -147,6 +218,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
canceled,
|
||||
options);
|
||||
applyDeptOutcome(result, outcome, "部门", dept.getDepartmentname());
|
||||
if (outcome.deptId() != null) {
|
||||
readyParentIds.add(outcome.deptId());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("[iWork] 同步部门失败: id={} name={}", dept.getId(), dept.getDepartmentname(), ex);
|
||||
result.increaseFailed();
|
||||
@@ -160,11 +234,42 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
}
|
||||
}
|
||||
if (!queue.isEmpty()) {
|
||||
for (IWorkHrDepartmentPageRespVO.Department remaining : queue) {
|
||||
log.warn("[iWork] 部门因父级缺失未同步: id={} name={}", remaining.getId(), remaining.getDepartmentname());
|
||||
result.increaseFailed();
|
||||
if (context != null && !allowPlaceholderOnRemaining) {
|
||||
context.getPendingDepartments().addAll(queue);
|
||||
queue.clear();
|
||||
}
|
||||
}
|
||||
if (!queue.isEmpty()) {
|
||||
for (IWorkHrDepartmentPageRespVO.Department remaining : queue) {
|
||||
log.warn("[iWork] 部门父级缺失,延迟生成编码插入占位: id={} name={}", remaining.getId(), remaining.getDepartmentname());
|
||||
DeptSaveReqVO saveReq = buildDepartmentSaveReq(remaining,
|
||||
remaining.getId() == null ? null : remaining.getId().longValue(),
|
||||
resolveDepartmentParent(remaining).parentId(),
|
||||
isCanceledFlag(remaining.getCanceled()));
|
||||
saveReq.setDelayCodeGeneration(true);
|
||||
try {
|
||||
DeptSyncOutcome outcome = upsertDept(saveReq.getId(), saveReq, isCanceledFlag(remaining.getCanceled()), options);
|
||||
applyDeptOutcome(result, outcome, "部门", remaining.getDepartmentname());
|
||||
if (context != null && outcome.deptId() != null) {
|
||||
context.getPlaceholderDeptIds().add(outcome.deptId());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("[iWork] 部门占位插入失败: id={} name={}", remaining.getId(), remaining.getDepartmentname(), ex);
|
||||
result.increaseFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatchResult flushDeptPending(DeptSyncContext context, SyncOptions options) {
|
||||
BatchResult result = BatchResult.empty();
|
||||
if (context == null) {
|
||||
return result;
|
||||
}
|
||||
result.merge(syncSubcompaniesInternal(Collections.emptyList(), options, context, true));
|
||||
result.merge(syncDepartmentsInternal(Collections.emptyList(), options, context, true));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -236,7 +341,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE;
|
||||
// 直接沿用 iWork 原始密码,避免重复格式化造成校验偏差
|
||||
String externalPassword = trimToNull(user.getPassword());
|
||||
AdminUserDO existing = adminUserMapper.selectByUsername(username);
|
||||
AdminUserDO existing = adminUserMapper.selectById(user.getId());
|
||||
UserSyncOutcome outcome;
|
||||
if (existing == null) {
|
||||
if (!options.isCreateIfMissing()) {
|
||||
@@ -408,7 +513,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
req.setIsGroup(Boolean.FALSE);
|
||||
req.setDeptSource(DeptSourceEnum.IWORK.getSource());
|
||||
req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode());
|
||||
req.setExternalDeptCode(StrUtil.blankToDefault(trimToNull(data.getSubcompanycode()), String.valueOf(data.getId())));
|
||||
req.setExternalDeptCode(trimToNull(data.getSubcompanycode()));
|
||||
req.setExternalDeptName(data.getSubcompanyname());
|
||||
return req;
|
||||
}
|
||||
@@ -428,7 +533,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
req.setIsGroup(Boolean.FALSE);
|
||||
req.setDeptSource(DeptSourceEnum.IWORK.getSource());
|
||||
req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode());
|
||||
req.setExternalDeptCode(StrUtil.blankToDefault(trimToNull(data.getDepartmentcode()), String.valueOf(data.getId())));
|
||||
req.setExternalDeptCode(trimToNull(data.getDepartmentcode()));
|
||||
req.setExternalDeptName(data.getDepartmentname());
|
||||
return req;
|
||||
}
|
||||
@@ -493,6 +598,16 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
|
||||
return new ParentHolder(DeptDO.PARENT_ID_ROOT);
|
||||
}
|
||||
|
||||
private boolean isParentReady(Long parentId, Set<Long> readyParentIds) {
|
||||
if (parentId == null || DeptDO.PARENT_ID_ROOT.equals(parentId)) {
|
||||
return true;
|
||||
}
|
||||
if (readyParentIds.contains(parentId)) {
|
||||
return true;
|
||||
}
|
||||
return deptService.getDept(parentId) != null;
|
||||
}
|
||||
|
||||
private PostDO resolvePostByCode(String code) {
|
||||
String key = buildPostCacheKey(code);
|
||||
PostDO cached = postCache.get(key);
|
||||
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*;
|
||||
import com.zt.plat.module.system.enums.integration.IWorkSyncEntityTypeEnum;
|
||||
import com.zt.plat.module.system.service.dept.DeptService;
|
||||
import com.zt.plat.module.system.service.integration.iwork.IWorkOrgRestService;
|
||||
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor;
|
||||
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService;
|
||||
@@ -31,6 +32,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
|
||||
|
||||
private final IWorkOrgRestService orgRestService;
|
||||
private final IWorkSyncProcessor syncProcessor;
|
||||
private final DeptService deptService;
|
||||
|
||||
@Override
|
||||
public IWorkFullSyncRespVO fullSyncDepartments(IWorkFullSyncReqVO reqVO) {
|
||||
@@ -64,11 +66,14 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
|
||||
boolean syncJobTitle = scopes.contains(IWorkSyncEntityTypeEnum.JOB_TITLE);
|
||||
int processedPages = 0;
|
||||
IWorkSyncProcessor.SyncOptions options = buildFullSyncOptions(reqVO);
|
||||
IWorkSyncProcessor.DeptSyncContext deptSyncContext = (syncDepartments || syncSubcompanies)
|
||||
? new IWorkSyncProcessor.DeptSyncContext()
|
||||
: null;
|
||||
if (syncSubcompanies) {
|
||||
processedPages += executeSubcompanyFullSync(reqVO, options, respVO.getSubcompanyStat(), batchStats);
|
||||
processedPages += executeSubcompanyFullSync(reqVO, options, respVO.getSubcompanyStat(), batchStats, deptSyncContext);
|
||||
}
|
||||
if (syncDepartments) {
|
||||
processedPages += executeDepartmentFullSync(reqVO, options, respVO.getDepartmentStat(), batchStats);
|
||||
processedPages += executeDepartmentFullSync(reqVO, options, respVO.getDepartmentStat(), batchStats, deptSyncContext);
|
||||
}
|
||||
if (syncJobTitle) {
|
||||
processedPages += executeJobTitleFullSync(reqVO, options, respVO.getJobTitleStat(), batchStats);
|
||||
@@ -76,6 +81,13 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
|
||||
if (syncUsers) {
|
||||
processedPages += executeUserFullSync(reqVO, options, respVO.getUserStat(), batchStats);
|
||||
}
|
||||
if (deptSyncContext != null) {
|
||||
IWorkSyncProcessor.BatchResult flushResult = syncProcessor.flushDeptPending(deptSyncContext, options);
|
||||
updateStat(respVO.getDepartmentStat(), flushResult, 0);
|
||||
if (CollUtil.isNotEmpty(deptSyncContext.getPlaceholderDeptIds())) {
|
||||
deptService.backfillMissingCodesWithoutEvent(deptSyncContext.getPlaceholderDeptIds());
|
||||
}
|
||||
}
|
||||
respVO.setProcessedPages(processedPages);
|
||||
return respVO;
|
||||
}
|
||||
@@ -83,7 +95,8 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
|
||||
private int executeSubcompanyFullSync(IWorkFullSyncReqVO reqVO,
|
||||
IWorkSyncProcessor.SyncOptions options,
|
||||
IWorkSyncEntityStatVO stat,
|
||||
List<IWorkSyncBatchStatVO> batches) {
|
||||
List<IWorkSyncBatchStatVO> batches,
|
||||
IWorkSyncProcessor.DeptSyncContext context) {
|
||||
return executePaged(reqVO, IWorkSyncEntityTypeEnum.SUBCOMPANY, batches, (page, pageSize) -> {
|
||||
IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO();
|
||||
query.setCurpage(page);
|
||||
@@ -92,7 +105,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
|
||||
IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query);
|
||||
ensureIWorkSuccess("拉取分部", pageResp.isSuccess(), pageResp.getMessage());
|
||||
List<IWorkHrSubcompanyPageRespVO.Subcompany> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
|
||||
IWorkSyncProcessor.BatchResult result = syncProcessor.syncSubcompanies(dataList, options);
|
||||
IWorkSyncProcessor.BatchResult result = syncProcessor.syncSubcompanies(dataList, options, context);
|
||||
updateStat(stat, result, dataList.size());
|
||||
return new BatchExecution(result, dataList.size());
|
||||
});
|
||||
@@ -101,7 +114,8 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
|
||||
private int executeDepartmentFullSync(IWorkFullSyncReqVO reqVO,
|
||||
IWorkSyncProcessor.SyncOptions options,
|
||||
IWorkSyncEntityStatVO stat,
|
||||
List<IWorkSyncBatchStatVO> batches) {
|
||||
List<IWorkSyncBatchStatVO> batches,
|
||||
IWorkSyncProcessor.DeptSyncContext context) {
|
||||
return executePaged(reqVO, IWorkSyncEntityTypeEnum.DEPARTMENT, batches, (page, pageSize) -> {
|
||||
IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO();
|
||||
query.setCurpage(page);
|
||||
@@ -110,7 +124,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
|
||||
IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query);
|
||||
ensureIWorkSuccess("拉取部门", pageResp.isSuccess(), pageResp.getMessage());
|
||||
List<IWorkHrDepartmentPageRespVO.Department> dataList = CollUtil.emptyIfNull(pageResp.getDataList());
|
||||
IWorkSyncProcessor.BatchResult result = syncProcessor.syncDepartments(dataList, options);
|
||||
IWorkSyncProcessor.BatchResult result = syncProcessor.syncDepartments(dataList, options, context);
|
||||
updateStat(stat, result, dataList.size());
|
||||
return new BatchExecution(result, dataList.size());
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.zt.plat.module.system.service.permission;
|
||||
|
||||
import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
|
||||
import com.zt.plat.module.system.enums.permission.DataScopeEnum;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
@@ -143,4 +144,12 @@ public interface PermissionService {
|
||||
*/
|
||||
DeptDataPermissionRespDTO getDeptDataPermission(Long userId);
|
||||
|
||||
/**
|
||||
* 获得用户的数据权限级别
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @return 数据权限范围枚举
|
||||
*/
|
||||
DataScopeEnum getUserDataPermissionLevel(Long userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.zt.plat.module.system.enums.permission.RoleTypeEnum;
|
||||
import com.zt.plat.module.system.service.dept.DeptService;
|
||||
import com.zt.plat.module.system.service.user.AdminUserService;
|
||||
import com.zt.plat.module.system.service.userdept.UserDeptService;
|
||||
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -57,6 +58,15 @@ import static com.zt.plat.module.system.enums.ErrorCodeConstants.ROLE_CAN_NOT_UP
|
||||
@Slf4j
|
||||
public class PermissionServiceImpl implements PermissionService {
|
||||
|
||||
private static final List<DataScopeEnum> DATA_SCOPE_PRIORITY = Arrays.asList(
|
||||
DataScopeEnum.ALL,
|
||||
DataScopeEnum.COMPANY_AND_DEPT,
|
||||
DataScopeEnum.DEPT_AND_CHILD,
|
||||
DataScopeEnum.DEPT_ONLY,
|
||||
DataScopeEnum.DEPT_CUSTOM,
|
||||
DataScopeEnum.SELF
|
||||
);
|
||||
|
||||
@Resource
|
||||
private RoleMenuMapper roleMenuMapper;
|
||||
@Resource
|
||||
@@ -404,6 +414,40 @@ public class PermissionServiceImpl implements PermissionService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DataPermission(enable = false)
|
||||
@TenantIgnore
|
||||
public DataScopeEnum getUserDataPermissionLevel(Long userId) {
|
||||
List<RoleDO> roles = getEnableUserRoleListByUserIdFromCache(userId);
|
||||
if (CollUtil.isEmpty(roles)) {
|
||||
return DataScopeEnum.SELF;
|
||||
}
|
||||
|
||||
DataScopeEnum best = null;
|
||||
for (RoleDO role : roles) {
|
||||
DataScopeEnum scopeEnum = DataScopeEnum.findByScope(role.getDataScope());
|
||||
if (scopeEnum == null) {
|
||||
continue;
|
||||
}
|
||||
if (best == null || compareScope(scopeEnum, best) < 0) {
|
||||
best = scopeEnum;
|
||||
if (DataScopeEnum.ALL.equals(best)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best != null ? best : DataScopeEnum.SELF;
|
||||
}
|
||||
|
||||
private int compareScope(DataScopeEnum left, DataScopeEnum right) {
|
||||
return getScopePriority(left) - getScopePriority(right);
|
||||
}
|
||||
|
||||
private int getScopePriority(DataScopeEnum scope) {
|
||||
int idx = DATA_SCOPE_PRIORITY.indexOf(scope);
|
||||
return idx >= 0 ? idx : Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得自身的代理对象,解决 AOP 生效问题
|
||||
*
|
||||
|
||||
@@ -10,10 +10,10 @@ import com.zt.plat.module.system.dal.dataobject.sms.SmsCodeDO;
|
||||
import com.zt.plat.module.system.dal.mysql.sms.SmsCodeMapper;
|
||||
import com.zt.plat.module.system.enums.sms.SmsSceneEnum;
|
||||
import com.zt.plat.module.system.framework.sms.config.SmsCodeProperties;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static cn.hutool.core.util.RandomUtil.randomInt;
|
||||
@@ -56,11 +56,11 @@ public class SmsCodeServiceImpl implements SmsCodeService {
|
||||
if (lastSmsCode != null) {
|
||||
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
|
||||
< smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁
|
||||
throw exception(SMS_CODE_SEND_TOO_FAST);
|
||||
throw exception(SMS_CODE_SEND_TOO_FAST, smsCodeProperties.getSendFrequency().toMinutes());
|
||||
}
|
||||
if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限
|
||||
lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。
|
||||
throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY);
|
||||
throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY, smsCodeProperties.getSendMaximumQuantityPerDay());
|
||||
}
|
||||
// TODO ZT:提升,每个 IP 每天可发送数量
|
||||
// TODO ZT:提升,每个 IP 每小时可发送数量
|
||||
|
||||
@@ -193,10 +193,14 @@ public interface AdminUserService {
|
||||
* @param status 状态
|
||||
* @return 用户们
|
||||
*/
|
||||
List<AdminUserDO> getUserListByStatus(Integer status, Integer limit);
|
||||
List<AdminUserDO> getUserListByStatus(Integer status, Integer limit, String keyword);
|
||||
|
||||
default List<AdminUserDO> getUserListByStatus(Integer status, Integer limit) {
|
||||
return getUserListByStatus(status, limit, null);
|
||||
}
|
||||
|
||||
default List<AdminUserDO> getUserListByStatus(Integer status) {
|
||||
return getUserListByStatus(status, null);
|
||||
return getUserListByStatus(status, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -664,8 +664,8 @@ public class AdminUserServiceImpl implements AdminUserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AdminUserDO> getUserListByStatus(Integer status, Integer limit) {
|
||||
List<AdminUserDO> users = userMapper.selectListByStatus(status, limit);
|
||||
public List<AdminUserDO> getUserListByStatus(Integer status, Integer limit, String keyword) {
|
||||
List<AdminUserDO> users = userMapper.selectListByStatus(status, limit, keyword);
|
||||
fillUserDeptInfo(users);
|
||||
return users;
|
||||
}
|
||||
|
||||
@@ -241,8 +241,8 @@ zt:
|
||||
expire-times: 10m
|
||||
send-frequency: 1m
|
||||
send-maximum-quantity-per-day: 10
|
||||
begin-code: 9999 # 这里配置 9999 的原因是,测试方便。
|
||||
end-code: 9999 # 这里配置 9999 的原因是,测试方便。
|
||||
begin-code: 100000
|
||||
end-code: 999999
|
||||
|
||||
|
||||
# E办OAuth2配置文件
|
||||
|
||||
Reference in New Issue
Block a user