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:
hewencai
2025-12-24 11:15:23 +08:00
46 changed files with 1539 additions and 1266 deletions

View File

@@ -4,6 +4,7 @@ import com.zt.plat.framework.common.biz.system.permission.PermissionCommonApi;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.module.system.api.permission.dto.*;
import com.zt.plat.module.system.enums.ApiConstants;
import com.zt.plat.module.system.enums.permission.DataScopeEnum;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
@@ -50,4 +51,9 @@ public interface PermissionApi extends PermissionCommonApi {
@Parameter(name = "userId", description = "用户编号", example = "1", required = true)
CommonResult<Set<Long>> getUserRoleIdListByUserId(@RequestParam("userId") Long userId);
@GetMapping(PREFIX + "/user-data-permission-level")
@Operation(summary = "获得用户的数据权限级别")
@Parameter(name = "userId", description = "用户编号", example = "1", required = true)
CommonResult<DataScopeEnum> getUserDataPermissionLevel(@RequestParam("userId") Long userId);
}

View File

@@ -127,8 +127,8 @@ public interface ErrorCodeConstants {
ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在");
ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期");
ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用");
ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量");
ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频繁");
ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1_002_014_004, "超过每日短信发送数量:{}次");
ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频繁,请于{}分钟后再试");
// ========== 租户信息 1-002-015-000 ==========
ErrorCode TENANT_NOT_EXISTS = new ErrorCode(1_002_015_000, "租户不存在");

View File

@@ -1,10 +1,12 @@
package com.zt.plat.module.system.enums.permission;
import com.fasterxml.jackson.annotation.JsonValue;
import com.zt.plat.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 数据范围枚举类
@@ -33,6 +35,26 @@ public enum DataScopeEnum implements ArrayValuable<Integer> {
public static final Integer[] ARRAYS = Arrays.stream(values()).map(DataScopeEnum::getScope).toArray(Integer[]::new);
/**
* Jackson 序列化时输出整数 code兼容旧客户端
*/
@JsonValue
public Integer getScope() {
return scope;
}
public static DataScopeEnum findByScope(Integer scope) {
if (scope == null) {
return null;
}
for (DataScopeEnum value : values()) {
if (Objects.equals(value.scope, scope)) {
return value;
}
}
return null;
}
@Override
public Integer[] array() {
return ARRAYS;

View File

@@ -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));

View File

@@ -76,4 +76,7 @@ public class DeptSaveReqVO {
@Schema(description = "部门来源类型", example = "1")
private Integer deptSource;
@Schema(description = "内部使用:延迟生成部门编码", hidden = true)
private Boolean delayCodeGeneration;
}

View File

@@ -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));
}

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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);
}

View File

@@ -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());
// 按 sortnullsLast再按 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);
}
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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());
});

View File

@@ -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);
}

View File

@@ -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 生效问题
*

View File

@@ -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 每小时可发送数量

View File

@@ -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);
}
/**

View File

@@ -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;
}

View File

@@ -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配置文件

View File

@@ -3,23 +3,25 @@ package com.zt.plat.module.system.service.dept;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.common.util.object.ObjectUtils;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.module.system.controller.admin.dept.vo.depexternalcode.DeptExternalCodeSaveReqVO;
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.dept.vo.depexternalcode.DeptExternalCodeSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptExternalCodeMapper;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper;
import com.zt.plat.module.system.service.dept.DeptExternalCodeServiceImpl;
import com.zt.plat.module.system.dal.redis.RedisKeyConstants;
import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
import com.zt.plat.module.system.service.permission.PermissionService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import java.util.Arrays;
import java.util.List;
@@ -51,6 +53,9 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Resource
private CacheManager cacheManager;
@MockBean
private PermissionService permissionService;
@TestConfiguration
@EnableCaching
static class CacheConfig {
@@ -69,7 +74,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
reqVO.setName(name);
reqVO.setSort(sort);
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
reqVO.setDeptSource(1);
reqVO.setDeptSource(DeptSourceEnum.SYNC.getSource());
reqVO.setIsCompany(false);
reqVO.setIsGroup(false);
return deptService.createDept(reqVO);
@@ -83,7 +88,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setStatus(randomCommonStatus());
o.setCode(null);
}).setDeptSource(1);
}).setDeptSource(DeptSourceEnum.SYNC.getSource());
// 调用
Long deptId = deptService.createDept(reqVO);
@@ -105,7 +110,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
childReq.setName("事业部");
childReq.setSort(1);
childReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
childReq.setDeptSource(1);
childReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long childId = deptService.createDept(childReq);
DeptDO childDept = deptMapper.selectById(childId);
@@ -119,7 +124,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
topLevelReq.setName("总部");
topLevelReq.setSort(1);
topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
topLevelReq.setDeptSource(1);
topLevelReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long topLevelId = deptService.createDept(topLevelReq);
DeptDO firstTop = deptMapper.selectById(topLevelId);
assertEquals("ZT001", firstTop.getCode());
@@ -129,12 +134,185 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
secondTopLevelReq.setName("总部");
secondTopLevelReq.setSort(2);
secondTopLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
secondTopLevelReq.setDeptSource(1);
secondTopLevelReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long secondTopId = deptService.createDept(secondTopLevelReq);
DeptDO secondTop = deptMapper.selectById(secondTopId);
assertEquals("ZT002", secondTop.getCode());
}
@Test
public void testCreateDept_externalUsesCuPrefixAndIndependentSequence() {
// 自建 EXTERNAL 顶级生成 CU001且不受 ZT 序列影响
DeptSaveReqVO externalTop = new DeptSaveReqVO();
externalTop.setParentId(DeptDO.PARENT_ID_ROOT);
externalTop.setName("自建总部");
externalTop.setSort(1);
externalTop.setStatus(CommonStatusEnum.ENABLE.getStatus());
externalTop.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
Long cuTopId = deptService.createDept(externalTop);
DeptDO cuTop = deptMapper.selectById(cuTopId);
assertEquals("CU001", cuTop.getCode());
// 同时创建同步来源(非 EXTERNAL仍使用 ZT 序列
DeptSaveReqVO syncTop = new DeptSaveReqVO();
syncTop.setParentId(DeptDO.PARENT_ID_ROOT);
syncTop.setName("同步总部");
syncTop.setSort(2);
syncTop.setStatus(CommonStatusEnum.ENABLE.getStatus());
syncTop.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long ztTopId = deptService.createDept(syncTop);
DeptDO ztTop = deptMapper.selectById(ztTopId);
assertEquals("ZT001", ztTop.getCode());
// 再创建一个自建顶级,应独立递增为 CU002
DeptSaveReqVO externalTop2 = new DeptSaveReqVO();
externalTop2.setParentId(DeptDO.PARENT_ID_ROOT);
externalTop2.setName("自建二部");
externalTop2.setSort(3);
externalTop2.setStatus(CommonStatusEnum.ENABLE.getStatus());
externalTop2.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
Long cuTop2Id = deptService.createDept(externalTop2);
DeptDO cuTop2 = deptMapper.selectById(cuTop2Id);
assertEquals("CU002", cuTop2.getCode());
}
@Test
public void testCreateDept_externalChildFollowsCuPrefix() {
DeptSaveReqVO externalTop = new DeptSaveReqVO();
externalTop.setParentId(DeptDO.PARENT_ID_ROOT);
externalTop.setName("自建根");
externalTop.setSort(1);
externalTop.setStatus(CommonStatusEnum.ENABLE.getStatus());
externalTop.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
Long topId = deptService.createDept(externalTop);
DeptDO top = deptMapper.selectById(topId);
assertEquals("CU001", top.getCode());
DeptSaveReqVO childReq = new DeptSaveReqVO();
childReq.setParentId(topId);
childReq.setName("自建子");
childReq.setSort(1);
childReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
childReq.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
Long childId = deptService.createDept(childReq);
DeptDO child = deptMapper.selectById(childId);
assertEquals("CU001001", child.getCode());
}
@Test
public void testCreateDept_externalChildUnderSyncParentUsesCuPrefix() {
// 同步来源父级,使用 ZT 序列
DeptSaveReqVO syncTop = new DeptSaveReqVO();
syncTop.setParentId(DeptDO.PARENT_ID_ROOT);
syncTop.setName("同步父");
syncTop.setSort(1);
syncTop.setStatus(CommonStatusEnum.ENABLE.getStatus());
syncTop.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long syncTopId = deptService.createDept(syncTop);
DeptDO syncTopDept = deptMapper.selectById(syncTopId);
assertEquals("ZT001", syncTopDept.getCode());
// 在同步父级下新增外部子部门,前缀替换为 CU序列与 ZT 独立
DeptSaveReqVO externalChild1 = new DeptSaveReqVO();
externalChild1.setParentId(syncTopId);
externalChild1.setName("外部子1");
externalChild1.setSort(1);
externalChild1.setStatus(CommonStatusEnum.ENABLE.getStatus());
externalChild1.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
Long child1Id = deptService.createDept(externalChild1);
DeptDO child1 = deptMapper.selectById(child1Id);
assertEquals("CU001001", child1.getCode());
DeptSaveReqVO externalChild2 = new DeptSaveReqVO();
externalChild2.setParentId(syncTopId);
externalChild2.setName("外部子2");
externalChild2.setSort(2);
externalChild2.setStatus(CommonStatusEnum.ENABLE.getStatus());
externalChild2.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
Long child2Id = deptService.createDept(externalChild2);
DeptDO child2 = deptMapper.selectById(child2Id);
assertEquals("CU001002", child2.getCode());
// 同步子部门仍使用 ZT 序列,不受 CU 序列影响
DeptSaveReqVO syncChild = new DeptSaveReqVO();
syncChild.setParentId(syncTopId);
syncChild.setName("同步子");
syncChild.setSort(3);
syncChild.setStatus(CommonStatusEnum.ENABLE.getStatus());
syncChild.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long syncChildId = deptService.createDept(syncChild);
DeptDO syncChildDept = deptMapper.selectById(syncChildId);
assertEquals("ZT001001", syncChildDept.getCode());
}
@Test
public void testCreateDept_iWorkFollowsGenerationAndIgnoresCustomCode() {
// iWork 顶级也应走 ZT 序列,忽略自定义 code
DeptSaveReqVO iworkTop = new DeptSaveReqVO();
iworkTop.setParentId(DeptDO.PARENT_ID_ROOT);
iworkTop.setName("iWork 顶级");
iworkTop.setSort(1);
iworkTop.setStatus(CommonStatusEnum.ENABLE.getStatus());
iworkTop.setDeptSource(DeptSourceEnum.IWORK.getSource());
iworkTop.setCode("CUSTOM-ZT999");
Long topId = deptService.createDept(iworkTop);
DeptDO top = deptMapper.selectById(topId);
assertEquals("ZT001", top.getCode());
// 子级继承序列递增
DeptSaveReqVO childReq = new DeptSaveReqVO();
childReq.setParentId(topId);
childReq.setName("iWork 子");
childReq.setSort(1);
childReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
childReq.setDeptSource(DeptSourceEnum.IWORK.getSource());
Long childId = deptService.createDept(childReq);
DeptDO child = deptMapper.selectById(childId);
assertEquals("ZT001001", child.getCode());
}
@Test
public void testUpdateDept_iWorkGeneratesOnceWhenMissingCode() {
// 先创建一个 iWork 顶级但延迟生成编码
DeptSaveReqVO createReq = new DeptSaveReqVO();
createReq.setParentId(DeptDO.PARENT_ID_ROOT);
createReq.setName("iWork 延迟");
createReq.setSort(1);
createReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
createReq.setDeptSource(DeptSourceEnum.IWORK.getSource());
createReq.setDelayCodeGeneration(true);
Long deptId = deptService.createDept(createReq);
DeptDO created = deptMapper.selectById(deptId);
assertNull(created.getCode());
// 更新时生成一次编码
DeptSaveReqVO updateReq = new DeptSaveReqVO();
updateReq.setId(deptId);
updateReq.setParentId(DeptDO.PARENT_ID_ROOT);
updateReq.setName("iWork 延迟");
updateReq.setSort(1);
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setDeptSource(DeptSourceEnum.IWORK.getSource());
updateReq.setDelayCodeGeneration(false);
deptService.updateDept(updateReq);
DeptDO updated = deptMapper.selectById(deptId);
assertEquals("ZT001", updated.getCode());
// 再次更新(父级不变)保持编码不变
DeptSaveReqVO updateReq2 = new DeptSaveReqVO();
updateReq2.setId(deptId);
updateReq2.setParentId(DeptDO.PARENT_ID_ROOT);
updateReq2.setName("iWork 延迟2");
updateReq2.setSort(2);
updateReq2.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq2.setDeptSource(DeptSourceEnum.IWORK.getSource());
deptService.updateDept(updateReq2);
DeptDO updated2 = deptMapper.selectById(deptId);
assertEquals("ZT001", updated2.getCode());
}
@Test
public void testCreateDept_topLevelAutoCode_ignoreCustomInput() {
String customCode = "ROOT-001";
@@ -143,7 +321,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
topLevelReq.setName("集团");
topLevelReq.setSort(1);
topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
topLevelReq.setDeptSource(1);
topLevelReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
topLevelReq.setCode(customCode);
Long deptId = deptService.createDept(topLevelReq);
@@ -166,7 +344,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setId(dbDeptDO.getId());
o.setStatus(randomCommonStatus());
}).setDeptSource(1);
}).setDeptSource(DeptSourceEnum.SYNC.getSource());
reqVO.setCode(dbDeptDO.getCode());
// 调用
@@ -195,7 +373,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq.setParentId(parentBId);
updateReq.setSort(1);
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setDeptSource(1);
updateReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
deptService.updateDept(updateReq);
DeptDO updatedChild = deptMapper.selectById(childId);
@@ -223,7 +401,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq1.setName("多系统部门");
updateReq1.setSort(1);
updateReq1.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq1.setDeptSource(1);
updateReq1.setDeptSource(DeptSourceEnum.SYNC.getSource());
updateReq1.setExternalSystemCode("ERP");
updateReq1.setExternalDeptCode("ERP-100");
deptService.updateDept(updateReq1);
@@ -235,7 +413,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq2.setName("多系统部门");
updateReq2.setSort(1);
updateReq2.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq2.setDeptSource(1);
updateReq2.setDeptSource(DeptSourceEnum.SYNC.getSource());
updateReq2.setExternalSystemCode("OA");
updateReq2.setExternalDeptCode("OA-100");
deptService.updateDept(updateReq2);
@@ -257,7 +435,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
createA.setName("iWork-A");
createA.setSort(1);
createA.setStatus(CommonStatusEnum.ENABLE.getStatus());
createA.setDeptSource(1);
createA.setDeptSource(DeptSourceEnum.SYNC.getSource());
createA.setExternalSystemCode("IWORK");
createA.setExternalDeptCode("IW-001");
Long deptAId = deptService.createDept(createA);
@@ -272,7 +450,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
createB.setName("iWork-B");
createB.setSort(2);
createB.setStatus(CommonStatusEnum.ENABLE.getStatus());
createB.setDeptSource(1);
createB.setDeptSource(DeptSourceEnum.SYNC.getSource());
createB.setExternalSystemCode("IWORK");
createB.setExternalDeptCode("IW-001");
Long deptBId = deptService.createDept(createB);
@@ -300,7 +478,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq.setName("子-更新");
updateReq.setSort(1);
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setDeptSource(1);
updateReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
updateReq.setExternalSystemCode("IWORK");
updateReq.setExternalDeptCode("IW-CHILD");
@@ -474,7 +652,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test
public void testGetDept() {
// mock 数据
DeptDO deptDO = randomPojo(DeptDO.class).setDeptSource(1);
DeptDO deptDO = randomPojo(DeptDO.class).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(deptDO);
// 准备参数
Long id = deptDO.getId();
@@ -488,9 +666,9 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test
public void testGetDeptList_ids() {
// mock 数据
DeptDO deptDO01 = randomPojo(DeptDO.class).setDeptSource(1);
DeptDO deptDO01 = randomPojo(DeptDO.class).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(deptDO01);
DeptDO deptDO02 = randomPojo(DeptDO.class).setDeptSource(1);
DeptDO deptDO02 = randomPojo(DeptDO.class).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(deptDO02);
// 准备参数
List<Long> ids = Arrays.asList(deptDO01.getId(), deptDO02.getId());
@@ -511,7 +689,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setSort(1);
}).setDeptSource(1);
}).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept);
// 测试 name 不匹配
deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> {
@@ -542,14 +720,14 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setSort(1);
}).setDeptSource(1);
}).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept1);
DeptDO dept2 = randomPojo(DeptDO.class, o -> {
o.setName("集团二部");
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setSort(2);
}).setDeptSource(1);
}).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept2);
DeptDO otherDept = randomPojo(DeptDO.class, o -> {
o.setName("其他部门");
@@ -573,14 +751,14 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test
public void testGetChildDeptList() {
// mock 数据1 级别子节点)
DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")).setDeptSource(1);
DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept1);
DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")).setDeptSource(1);
DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept2);
// mock 数据2 级子节点)
DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())).setDeptSource(1);
DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept1a);
DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())).setDeptSource(1);
DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept2a);
// 准备参数
Long id = dept1.getParentId();
@@ -596,14 +774,14 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test
public void testGetChildDeptListFromCache() {
// mock 数据1 级别子节点)
DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")).setDeptSource(1);
DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept1);
DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")).setDeptSource(1);
DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept2);
// mock 数据2 级子节点)
DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())).setDeptSource(1);
DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept1a);
DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())).setDeptSource(1);
DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept2a);
// 准备参数
Long id = dept1.getParentId();
@@ -689,4 +867,48 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
assertEquals("ZT001002", updatedChild2.getCode());
}
@Test
public void testCreateDept_delayCodeGeneration_thenGenerateWhenParentReady() {
Long missingParentId = 900L;
DeptSaveReqVO childReq = new DeptSaveReqVO();
childReq.setParentId(missingParentId);
childReq.setName("延迟子部门");
childReq.setSort(1);
childReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
childReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
childReq.setDelayCodeGeneration(true);
Long childId = deptService.createDept(childReq);
DeptDO child = deptMapper.selectById(childId);
assertNotNull(childId);
assertNull(child.getCode());
// 后补父级并赋予编码
DeptDO parent = new DeptDO();
parent.setId(missingParentId);
parent.setParentId(DeptDO.PARENT_ID_ROOT);
parent.setName("后补父级");
parent.setCode("ZT900");
parent.setSort(1);
parent.setStatus(CommonStatusEnum.ENABLE.getStatus());
parent.setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(parent);
// 触发子部门生成编码
DeptSaveReqVO updateReq = new DeptSaveReqVO();
updateReq.setId(childId);
updateReq.setParentId(missingParentId);
updateReq.setName("延迟子部门");
updateReq.setSort(1);
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
updateReq.setDelayCodeGeneration(false);
deptService.updateDept(updateReq);
DeptDO updatedChild = deptMapper.selectById(childId);
assertEquals(parent.getCode() + "001", updatedChild.getCode());
}
}

View File

@@ -0,0 +1,141 @@
package com.zt.plat.module.system.service.integration.iwork.impl;
import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrSubcompanyPageRespVO;
import com.zt.plat.module.system.dal.mysql.dept.PostMapper;
import com.zt.plat.module.system.dal.mysql.user.AdminUserMapper;
import com.zt.plat.module.system.service.dept.DeptService;
import com.zt.plat.module.system.service.dept.PostService;
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor;
import com.zt.plat.module.system.service.user.AdminUserService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* Tests for cross-page pending handling and placeholder backfill in IWorkSyncProcessorImpl.
*/
class IWorkSyncProcessorImplTest extends BaseMockitoUnitTest {
@InjectMocks
private IWorkSyncProcessorImpl processor;
@Mock
private DeptService deptService;
@Mock
private PostService postService;
@Mock
private PostMapper postMapper;
@Mock
private AdminUserService adminUserService;
@Mock
private AdminUserMapper adminUserMapper;
@Test
void shouldProcessPendingChildWhenParentArrivesInLaterPage() {
IWorkSyncProcessor.DeptSyncContext context = new IWorkSyncProcessor.DeptSyncContext();
IWorkSyncProcessor.SyncOptions options = IWorkSyncProcessor.SyncOptions.custom(true, true, true);
IWorkHrDepartmentPageRespVO.Department child = new IWorkHrDepartmentPageRespVO.Department();
child.setId(200);
child.setDepartmentname("child");
child.setSupdepid(100); // parent comes later
IWorkHrDepartmentPageRespVO.Department parent = new IWorkHrDepartmentPageRespVO.Department();
parent.setId(100);
parent.setDepartmentname("parent");
parent.setSupdepid(0); // root
when(deptService.getDept(anyLong())).thenReturn(null);
when(deptService.createDept(any(DeptSaveReqVO.class))).thenReturn(100L, 200L);
processor.syncDepartments(List.of(child), options, context);
verify(deptService, never()).createDept(any());
assertEquals(1, context.getPendingDepartments().size());
processor.syncDepartments(List.of(parent), options, context);
verify(deptService, times(2)).createDept(any());
assertTrue(context.getPendingDepartments().isEmpty(), "pending should be cleared after parent processed");
}
@Test
void shouldInsertPlaceholderWhenParentMissingAfterFlush() {
IWorkSyncProcessor.DeptSyncContext context = new IWorkSyncProcessor.DeptSyncContext();
IWorkSyncProcessor.SyncOptions options = IWorkSyncProcessor.SyncOptions.custom(true, true, true);
IWorkHrDepartmentPageRespVO.Department child = new IWorkHrDepartmentPageRespVO.Department();
child.setId(300);
child.setDepartmentname("orphan");
child.setSupdepid(9999); // never provided
when(deptService.getDept(anyLong())).thenReturn(null);
when(deptService.createDept(any(DeptSaveReqVO.class))).thenReturn(300L);
processor.syncDepartments(List.of(child), options, context);
assertEquals(1, context.getPendingDepartments().size());
IWorkSyncProcessor.BatchResult flushResult = processor.flushDeptPending(context, options);
assertNotNull(flushResult);
ArgumentCaptor<DeptSaveReqVO> captor = ArgumentCaptor.forClass(DeptSaveReqVO.class);
verify(deptService, times(1)).createDept(captor.capture());
DeptSaveReqVO placeholderReq = captor.getValue();
assertTrue(Boolean.TRUE.equals(placeholderReq.getDelayCodeGeneration()));
assertNull(placeholderReq.getCode());
assertTrue(context.getPendingDepartments().isEmpty(), "pending should be cleared after placeholder insert");
assertTrue(context.getPlaceholderDeptIds().contains(300L));
}
@Test
void shouldKeepExternalCodeNullWhenDepartmentCodeBlank() {
IWorkSyncProcessor.SyncOptions options = IWorkSyncProcessor.SyncOptions.custom(true, true, true);
IWorkHrDepartmentPageRespVO.Department dept = new IWorkHrDepartmentPageRespVO.Department();
dept.setId(500);
dept.setDepartmentname("blank-code-dept");
dept.setDepartmentcode(" ");
dept.setSupdepid(0);
when(deptService.getDept(anyLong())).thenReturn(null);
when(deptService.createDept(any(DeptSaveReqVO.class))).thenReturn(500L);
processor.syncDepartments(List.of(dept), options, null);
ArgumentCaptor<DeptSaveReqVO> captor = ArgumentCaptor.forClass(DeptSaveReqVO.class);
verify(deptService, times(1)).createDept(captor.capture());
DeptSaveReqVO req = captor.getValue();
assertNull(req.getExternalDeptCode(), "externalDeptCode should remain null when source code is blank");
}
@Test
void shouldKeepExternalCodeNullWhenSubcompanyCodeBlank() {
IWorkSyncProcessor.SyncOptions options = IWorkSyncProcessor.SyncOptions.custom(true, true, true);
IWorkHrSubcompanyPageRespVO.Subcompany subcompany = new IWorkHrSubcompanyPageRespVO.Subcompany();
subcompany.setId(600);
subcompany.setSubcompanyname("blank-code-sub");
subcompany.setSubcompanycode(null);
subcompany.setSupsubcomid(0);
when(deptService.getDept(anyLong())).thenReturn(null);
when(deptService.createDept(any(DeptSaveReqVO.class))).thenReturn(600L);
processor.syncSubcompanies(List.of(subcompany), options, null);
ArgumentCaptor<DeptSaveReqVO> captor = ArgumentCaptor.forClass(DeptSaveReqVO.class);
verify(deptService, times(1)).createDept(captor.capture());
DeptSaveReqVO req = captor.getValue();
assertNull(req.getExternalDeptCode(), "externalDeptCode should remain null when source code is null or blank");
}
}

View File

@@ -0,0 +1,62 @@
package com.zt.plat.module.system.service.integration.iwork.impl;
import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkFullSyncReqVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrDepartmentPageRespVO;
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 org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.stubbing.Answer;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
class IWorkSyncServiceImplTest extends BaseMockitoUnitTest {
@InjectMocks
private IWorkSyncServiceImpl syncService;
@Mock
private IWorkOrgRestService orgRestService;
@Mock
private IWorkSyncProcessor syncProcessor;
@Mock
private DeptService deptService;
@Test
void shouldBackfillCodesWhenPlaceholdersExistAfterFullSync() {
IWorkFullSyncReqVO reqVO = new IWorkFullSyncReqVO();
reqVO.setPageSize(1);
reqVO.setMaxPages(1);
IWorkHrDepartmentPageRespVO pageResp = new IWorkHrDepartmentPageRespVO();
pageResp.setSuccess(true);
IWorkHrDepartmentPageRespVO.Department dept = new IWorkHrDepartmentPageRespVO.Department();
dept.setId(1);
pageResp.setDataList(List.of(dept));
when(orgRestService.listDepartments(any())).thenReturn(pageResp);
// 在部门同步时标记占位 ID
doAnswer((Answer<IWorkSyncProcessor.BatchResult>) invocation -> {
IWorkSyncProcessor.DeptSyncContext context = invocation.getArgument(2);
if (context != null) {
context.getPlaceholderDeptIds().add(500L);
}
return IWorkSyncProcessor.BatchResult.empty();
}).when(syncProcessor).syncDepartments(any(), any(), any(IWorkSyncProcessor.DeptSyncContext.class));
when(syncProcessor.flushDeptPending(any(), any())).thenReturn(IWorkSyncProcessor.BatchResult.empty());
syncService.fullSyncDepartments(reqVO);
verify(deptService, times(1)).backfillMissingCodesWithoutEvent(argThat(set -> set.contains(500L)));
}
}

View File

@@ -1,7 +1,9 @@
package com.zt.plat.module.system.service.permission;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.enums.CommonStatusEnum;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.framework.common.util.json.JsonUtils;
import com.zt.plat.module.system.controller.admin.permission.vo.role.RoleSaveReqVO;
import com.zt.plat.module.system.dal.dataobject.permission.RoleDO;
import com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDO;
@@ -11,6 +13,7 @@ import com.zt.plat.module.system.dal.mysql.permission.RoleMapper;
import com.zt.plat.module.system.dal.mysql.permission.RoleMenuMapper;
import com.zt.plat.module.system.dal.mysql.permission.UserRoleMapper;
import com.zt.plat.module.system.dal.mysql.rolemenuexclusion.RoleMenuExclusionMapper;
import com.zt.plat.module.system.enums.permission.DataScopeEnum;
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;
@@ -408,4 +411,54 @@ public class PermissionServiceTest extends BaseDbUnitTest {
assertEquals(1, exclusionDOS.size());
assertEquals(101L, exclusionDOS.get(0).getMenuId());
}
@Test
public void testGetUserDataPermissionLevel_noRolesReturnSelf() {
Long userId = 1000L;
DataScopeEnum result = permissionService.getUserDataPermissionLevel(userId);
assertEquals(DataScopeEnum.SELF, result);
}
@Test
public void testGetUserDataPermissionLevel_pickHighestPriority() {
Long userId = 2000L;
RoleDO roleCustom = randomPojo(RoleDO.class, o -> o
.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setDataScope(DataScopeEnum.DEPT_CUSTOM.getScope())
.setId(110L)
.setTenantId(0L));
roleMapper.insert(roleCustom);
RoleDO roleCompany = randomPojo(RoleDO.class, o -> o
.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setDataScope(DataScopeEnum.COMPANY_AND_DEPT.getScope())
.setId(120L)
.setTenantId(0L));
roleMapper.insert(roleCompany);
userRoleMapper.insert(randomPojo(UserRoleDO.class, o -> o.setUserId(userId).setRoleId(roleCustom.getId())));
userRoleMapper.insert(randomPojo(UserRoleDO.class, o -> o.setUserId(userId).setRoleId(roleCompany.getId())));
DataScopeEnum result = permissionService.getUserDataPermissionLevel(userId);
assertEquals(DataScopeEnum.COMPANY_AND_DEPT, result);
}
@Test
public void testGetUserDataPermissionLevel_serializeAsNumber() {
Long userId = 3000L;
RoleDO roleAll = randomPojo(RoleDO.class, o -> o
.setStatus(CommonStatusEnum.ENABLE.getStatus())
.setDataScope(DataScopeEnum.ALL.getScope())
.setId(210L)
.setTenantId(0L));
roleMapper.insert(roleAll);
userRoleMapper.insert(randomPojo(UserRoleDO.class, o -> o.setUserId(userId).setRoleId(roleAll.getId())));
DataScopeEnum result = permissionService.getUserDataPermissionLevel(userId);
assertEquals(DataScopeEnum.ALL, result);
assertEquals("1", JsonUtils.toJsonString(result));
}
}

View File

@@ -12,7 +12,7 @@ create table IF NOT EXISTS system_user_dept (
);
CREATE TABLE IF NOT EXISTS "system_dept" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"code" varchar(64) NOT NULL DEFAULT '',
"code" varchar(64) DEFAULT NULL,
"name" varchar(30) NOT NULL DEFAULT '',
"short_name" varchar(30) DEFAULT '',
"parent_id" bigint NOT NULL DEFAULT '0',
@@ -51,9 +51,8 @@ CREATE TABLE IF NOT EXISTS "system_dept_external_code" (
PRIMARY KEY ("id")
) COMMENT '部门外部组织编码映射';
CREATE UNIQUE INDEX IF NOT EXISTS "uk_system_dept_external_code_ext" ON "system_dept_external_code" ("tenant_id", "system_code", "external_dept_code");
CREATE UNIQUE INDEX IF NOT EXISTS "uk_system_dept_external_code_dept" ON "system_dept_external_code" ("tenant_id", "system_code", "dept_id");
CREATE INDEX IF NOT EXISTS "idx_system_dept_external_code_dept" ON "system_dept_external_code" ("tenant_id", "dept_id");
CREATE INDEX IF NOT EXISTS "idx_system_dept_external_code_ext" ON "system_dept_external_code" ("tenant_id", "system_code", "external_dept_code");
CREATE INDEX IF NOT EXISTS "idx_system_dept_external_code_dept" ON "system_dept_external_code" ("tenant_id", "system_code", "dept_id");
CREATE TABLE IF NOT EXISTS "system_dict_data" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,