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

@@ -32,7 +32,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>3.0.45</revision> <revision>3.0.46</revision>
<!-- Maven 相关 --> <!-- Maven 相关 -->
<java.version>17</java.version> <java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
@@ -271,7 +271,8 @@
<profile> <profile>
<id>chenbowen</id> <id>chenbowen</id>
<properties> <properties>
<env.name>local</env.name> <!-- <env.name>local</env.name>-->
<env.name>dev</env.name>
<!-- <config.server-addr>localhost:8848</config.server-addr>--> <!-- <config.server-addr>localhost:8848</config.server-addr>-->
<config.server-addr>172.16.46.63:30848</config.server-addr> <config.server-addr>172.16.46.63:30848</config.server-addr>
<config.namespace>chenbowen</config.namespace> <config.namespace>chenbowen</config.namespace>

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,8 @@ CREATE TABLE databus_api_definition_credential (
deleted BIT DEFAULT '0' NOT NULL deleted BIT DEFAULT '0' NOT NULL
); );
CREATE UNIQUE INDEX uk_databus_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted); -- 去掉错误的唯一索引逻辑
-- CREATE UNIQUE INDEX uk_databus_api_definition_credential ON databus_api_definition_credential (api_id, credential_id, deleted);
CREATE INDEX idx_databus_api_definition_credential_api ON databus_api_definition_credential (api_id); CREATE INDEX idx_databus_api_definition_credential_api ON databus_api_definition_credential (api_id);
CREATE INDEX idx_databus_api_definition_credential_cred ON databus_api_definition_credential (credential_id); CREATE INDEX idx_databus_api_definition_credential_cred ON databus_api_definition_credential (credential_id);

View File

@@ -26,7 +26,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>3.0.45</revision> <revision>3.0.46</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<spring.boot.version>3.4.5</spring.boot.version> <spring.boot.version>3.4.5</spring.boot.version>

View File

@@ -1,6 +1,7 @@
package com.zt.plat.framework.common.util.security; package com.zt.plat.framework.common.util.security;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
import com.zt.plat.framework.common.util.json.JsonUtils;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.KeyGenerator; import javax.crypto.KeyGenerator;
@@ -126,7 +127,11 @@ public final class CryptoSignatureUtils {
continue; continue;
} }
sb.append(key).append('='); sb.append(key).append('=');
sb.append(value); if (value instanceof String || value instanceof Number || value instanceof Boolean) {
sb.append(value);
} else {
sb.append(JsonUtils.toJsonString(value));
}
sb.append('&'); sb.append('&');
} }
if (sb.length() > 0) { if (sb.length() > 0) {

View File

@@ -7,6 +7,8 @@ import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
@@ -18,14 +20,12 @@ import java.util.Set;
public class BusinessDataPermissionConfiguration { public class BusinessDataPermissionConfiguration {
@Bean @Bean
public BusinessDataPermissionEntityScanner businessDataPermissionEntityScanner(BeanFactory beanFactory, ApplicationContext applicationContext) { public BusinessDataPermissionEntityScanner businessDataPermissionEntityScanner(BeanFactory beanFactory, ApplicationContext applicationContext, Environment environment) {
Set<String> basePackages = new LinkedHashSet<>(); Set<String> basePackages = new LinkedHashSet<>();
addConfiguredBasePackages(environment, basePackages);
if (AutoConfigurationPackages.has(beanFactory)) { if (AutoConfigurationPackages.has(beanFactory)) {
basePackages.addAll(AutoConfigurationPackages.get(beanFactory)); basePackages.addAll(AutoConfigurationPackages.get(beanFactory));
} }
if (basePackages.isEmpty()) {
basePackages.add("com.zt");
}
ClassLoader classLoader = applicationContext != null ClassLoader classLoader = applicationContext != null
? applicationContext.getClassLoader() ? applicationContext.getClassLoader()
: Thread.currentThread().getContextClassLoader(); : Thread.currentThread().getContextClassLoader();
@@ -35,6 +35,21 @@ public class BusinessDataPermissionConfiguration {
return new BusinessDataPermissionEntityScanner(basePackages, classLoader); return new BusinessDataPermissionEntityScanner(basePackages, classLoader);
} }
private void addConfiguredBasePackages(Environment environment, Set<String> basePackages) {
if (environment == null) {
return;
}
String configured = environment.getProperty("zt.info.base-package");
if (!StringUtils.hasText(configured)) {
return;
}
for (String pkg : configured.split("[,;\\s]+")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg.trim());
}
}
}
@Bean @Bean
public CompanyDataPermissionRuleCustomizer autoCompanyDataPermissionRuleCustomizer(BusinessDataPermissionEntityScanner scanner) { public CompanyDataPermissionRuleCustomizer autoCompanyDataPermissionRuleCustomizer(BusinessDataPermissionEntityScanner scanner) {
return rule -> scanner.getEntityMetadata().forEach(metadata -> { return rule -> scanner.getEntityMetadata().forEach(metadata -> {

View File

@@ -28,6 +28,14 @@ import java.util.*;
@Slf4j @Slf4j
public class BusinessDataPermissionEntityScanner { public class BusinessDataPermissionEntityScanner {
/**
* 临时排除的包前缀(物流模块 DO不参与数据权限扫描
*/
private static final Set<String> EXCLUDED_PACKAGE_PREFIXES = Set.of(
"com.zt.plat.module.backendlogistics",
"com.zt.plat.module.erp",
"com.zt.plat.framework.mybatis.core.dataobject.BusinessBaseDO");
private final Set<String> basePackages; private final Set<String> basePackages;
private final ClassLoader classLoader; private final ClassLoader classLoader;
@@ -70,6 +78,9 @@ public class BusinessDataPermissionEntityScanner {
if (!StringUtils.hasText(className)) { if (!StringUtils.hasText(className)) {
continue; continue;
} }
if (isExcludedPackage(className)) {
continue;
}
try { try {
Class<?> clazz = ClassUtils.forName(className, classLoader); Class<?> clazz = ClassUtils.forName(className, classLoader);
if (clazz == BusinessBaseDO.class || !BusinessBaseDO.class.isAssignableFrom(clazz)) { if (clazz == BusinessBaseDO.class || !BusinessBaseDO.class.isAssignableFrom(clazz)) {
@@ -92,6 +103,15 @@ public class BusinessDataPermissionEntityScanner {
return new ArrayList<>(metadataMap.values()); return new ArrayList<>(metadataMap.values());
} }
private boolean isExcludedPackage(String className) {
for (String prefix : EXCLUDED_PACKAGE_PREFIXES) {
if (className.startsWith(prefix)) {
return true;
}
}
return false;
}
private EntityMetadata buildMetadata(Class<? extends BusinessBaseDO> entityClass) { private EntityMetadata buildMetadata(Class<? extends BusinessBaseDO> entityClass) {
String tableName = resolveTableName(entityClass); String tableName = resolveTableName(entityClass);
if (!StringUtils.hasText(tableName)) { if (!StringUtils.hasText(tableName)) {

View File

@@ -3,6 +3,7 @@ package com.zt.plat.framework.datapermission.core.rule.dept;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.zt.plat.framework.common.biz.system.permission.PermissionCommonApi; import com.zt.plat.framework.common.biz.system.permission.PermissionCommonApi;
import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
import com.zt.plat.framework.common.enums.UserTypeEnum; import com.zt.plat.framework.common.enums.UserTypeEnum;
@@ -14,7 +15,7 @@ import com.zt.plat.framework.mybatis.core.util.MyBatisUtils;
import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.LoginUser;
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Alias;
@@ -108,6 +109,11 @@ public class DeptDataPermissionRule implements DataPermissionRule {
return null; return null;
} }
// 显式忽略部门数据权限时直接放行
if (DeptContextHolder.shouldIgnore()) {
return null;
}
// 获得数据权限 // 获得数据权限
DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class);
// 从上下文中拿不到,则调用逻辑进行获取 // 从上下文中拿不到,则调用逻辑进行获取
@@ -136,6 +142,20 @@ public class DeptDataPermissionRule implements DataPermissionRule {
} }
} }
// 若存在部门上下文,优先使用上下文中的单一部门,必要时校验公司一致性
Long ctxDeptId = DeptContextHolder.getDeptId();
if (ctxDeptId != null && ctxDeptId > 0L) {
Long currentCompanyId = CompanyContextHolder.getCompanyId();
Long ctxCompanyId = DeptContextHolder.getCompanyId();
Long compareCompanyId = ctxCompanyId != null ? ctxCompanyId : currentCompanyId;
if (currentCompanyId != null && currentCompanyId > 0L
&& compareCompanyId != null && !currentCompanyId.equals(compareCompanyId)) {
log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptContextHolder company mismatch: currentCompanyId={}, ctxCompanyId={}, ctxDeptId={}, source=DeptContextHolder]",
JsonUtils.toJsonString(loginUser), tableName, tableAlias == null ? null : tableAlias.getName(),
currentCompanyId, compareCompanyId, ctxDeptId);
}
}
// 情况一,如果是 ALL 可查看全部,则无需拼接条件 // 情况一,如果是 ALL 可查看全部,则无需拼接条件
if (deptDataPermission.getAll()) { if (deptDataPermission.getAll()) {
return null; return null;

View File

@@ -7,10 +7,13 @@ import com.zt.plat.framework.common.enums.UserTypeEnum;
import com.zt.plat.framework.common.util.collection.SetUtils; import com.zt.plat.framework.common.util.collection.SetUtils;
import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.LoginUser;
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest; import com.zt.plat.framework.test.core.ut.BaseMockitoUnitTest;
import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Expression;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@@ -27,6 +30,7 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same; import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
/** /**
@@ -48,7 +52,13 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
// 清空 rule // 清空 rule
rule.getTableNames().clear(); rule.getTableNames().clear();
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); ((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear();
((Map<String, String>) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); ((Map<String, String>) ReflectUtil.getFieldValue(rule, "userColumns")).clear();
}
@AfterEach
void tearDown() {
DeptContextHolder.clear();
CompanyContextHolder.clear();
} }
@Test // 无 LoginUser @Test // 无 LoginUser
@@ -236,4 +246,88 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
} }
} }
@Test // 忽略部门数据权限,直接放行
void testGetExpression_ignoreDeptContext() {
try (MockedStatic<SecurityFrameworkUtils> secMock = mockStatic(SecurityFrameworkUtils.class);
MockedStatic<DeptContextHolder> deptCtxMock = mockStatic(DeptContextHolder.class)) {
String tableName = "t_order";
Alias alias = new Alias("o");
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(true);
Expression expression = rule.getExpression(tableName, alias);
assertNull(expression);
verifyNoInteractions(permissionApi);
}
}
@Test // 上下文部门存在且公司一致时,清空原集合并覆盖为单一 deptId
void testGetExpression_deptContextOverride_companyMatch() {
try (MockedStatic<SecurityFrameworkUtils> secMock = mockStatic(SecurityFrameworkUtils.class);
MockedStatic<DeptContextHolder> deptCtxMock = mockStatic(DeptContextHolder.class);
MockedStatic<CompanyContextHolder> companyCtxMock = mockStatic(CompanyContextHolder.class)) {
String tableName = "t_user";
Alias tableAlias = new Alias("u");
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
.setDeptIds(CollUtil.newLinkedHashSet(10L, 20L))
.setCompanyId(1L);
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(false);
deptCtxMock.when(DeptContextHolder::getDeptId).thenReturn(99L);
deptCtxMock.when(DeptContextHolder::getCompanyId).thenReturn(1L);
companyCtxMock.when(CompanyContextHolder::getCompanyId).thenReturn(1L);
companyCtxMock.when(CompanyContextHolder::isIgnore).thenReturn(false);
rule.addDeptColumn(tableName, "dept_id");
Expression expression = rule.getExpression(tableName, tableAlias);
assertEquals("u.dept_id IN (99)", expression.toString());
assertEquals(CollUtil.newLinkedHashSet(99L), deptDataPermission.getDeptIds());
assertEquals(1L, deptDataPermission.getCompanyId());
}
}
@Test // 上下文部门存在但公司不一致时,记录告警并保持原逻辑(不覆盖)
void testGetExpression_deptContextOverride_companyMismatch() {
try (MockedStatic<SecurityFrameworkUtils> secMock = mockStatic(SecurityFrameworkUtils.class);
MockedStatic<DeptContextHolder> deptCtxMock = mockStatic(DeptContextHolder.class);
MockedStatic<CompanyContextHolder> companyCtxMock = mockStatic(CompanyContextHolder.class)) {
String tableName = "t_user";
Alias tableAlias = new Alias("u");
LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L)
.setUserType(UserTypeEnum.ADMIN.getValue()));
secMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser);
DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO()
.setDeptIds(CollUtil.newLinkedHashSet(10L))
.setCompanyId(1L);
when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(success(deptDataPermission));
deptCtxMock.when(DeptContextHolder::shouldIgnore).thenReturn(false);
deptCtxMock.when(DeptContextHolder::getDeptId).thenReturn(99L);
deptCtxMock.when(DeptContextHolder::getCompanyId).thenReturn(2L);
companyCtxMock.when(CompanyContextHolder::getCompanyId).thenReturn(1L);
companyCtxMock.when(CompanyContextHolder::isIgnore).thenReturn(false);
rule.addDeptColumn(tableName, "dept_id");
Expression expression = rule.getExpression(tableName, tableAlias);
assertEquals("u.dept_id IN (10)", expression.toString());
assertEquals(CollUtil.newLinkedHashSet(10L), deptDataPermission.getDeptIds());
assertEquals(1L, deptDataPermission.getCompanyId());
}
}
} }

View File

@@ -0,0 +1,61 @@
package com.zt.plat.framework.tenant.core.context;
import com.alibaba.ttl.TransmittableThreadLocal;
/**
* 部门上下文 Holder使用 {@link TransmittableThreadLocal} 支持在线程池/异步场景下的上下文传递。
*
* 包含当前部门编号、所属公司编号以及是否忽略部门数据权限的标识。
*/
public class DeptContextHolder {
/** 当前部门编号 */
private static final ThreadLocal<Long> DEPT_ID = new TransmittableThreadLocal<>();
/** 当前部门所属公司编号(用于一致性校验) */
private static final ThreadLocal<Long> COMPANY_ID = new TransmittableThreadLocal<>();
/** 是否忽略部门数据权限 */
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
public static Long getDeptId() {
return DEPT_ID.get();
}
public static Long getCompanyId() {
return COMPANY_ID.get();
}
/**
* 设置部门与所属公司编号。
*/
public static void setContext(Long deptId, Long companyId) {
DEPT_ID.set(deptId);
COMPANY_ID.set(companyId);
}
public static void setDeptId(Long deptId) {
DEPT_ID.set(deptId);
}
public static void setCompanyId(Long companyId) {
COMPANY_ID.set(companyId);
}
public static boolean hasDeptId() {
Long deptId = DEPT_ID.get();
return deptId != null && deptId > 0L;
}
public static void setIgnore(Boolean ignore) {
IGNORE.set(ignore);
}
public static boolean shouldIgnore() {
return Boolean.TRUE.equals(IGNORE.get());
}
public static void clear() {
DEPT_ID.remove();
COMPANY_ID.remove();
IGNORE.remove();
}
}

View File

@@ -3,6 +3,7 @@ package com.zt.plat.framework.tenant.core.web;
import com.zt.plat.framework.security.core.LoginUser; import com.zt.plat.framework.security.core.LoginUser;
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils; import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder; import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
import com.zt.plat.framework.web.core.util.WebFrameworkUtils; import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@@ -66,11 +67,19 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor {
if (companyId == null || companyId <= 0L) { if (companyId == null || companyId <= 0L) {
CompanyContextHolder.setIgnore(true); CompanyContextHolder.setIgnore(true);
DeptContextHolder.clear();
return true; return true;
} }
CompanyContextHolder.setIgnore(false); CompanyContextHolder.setIgnore(false);
CompanyContextHolder.setCompanyId(companyId); CompanyContextHolder.setCompanyId(companyId);
// 默认不忽略部门数据权限;如果有有效部门则写入上下文
DeptContextHolder.setIgnore(false);
if (deptId != null && deptId > 0L) {
DeptContextHolder.setContext(deptId, companyId);
} else {
DeptContextHolder.clear();
}
if (loginUser == null) { if (loginUser == null) {
return true; return true;
} }
@@ -91,7 +100,9 @@ public class CompanyVisitContextInterceptor implements HandlerInterceptor {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser != null) { if (loginUser != null) {
loginUser.setVisitCompanyId(0L); loginUser.setVisitCompanyId(0L);
loginUser.setVisitDeptId(0L);
} }
DeptContextHolder.clear();
} }
private Long resolveLong(Object value) { private Long resolveLong(Object value) {

View File

@@ -0,0 +1,88 @@
package com.zt.plat.framework.tenant.core.web;
import com.zt.plat.framework.security.core.LoginUser;
import com.zt.plat.framework.tenant.core.context.CompanyContextHolder;
import com.zt.plat.framework.tenant.core.context.DeptContextHolder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import static org.junit.jupiter.api.Assertions.*;
/**
* CompanyVisitContextInterceptor 单测,覆盖公司/部门上下文写入及清理。
*/
class CompanyVisitContextInterceptorTest {
private final HandlerInterceptor interceptor = new CompanyVisitContextInterceptor();
@AfterEach
void tearDown() {
CompanyContextHolder.clear();
DeptContextHolder.clear();
SecurityContextHolder.clearContext();
}
@Test // 无公司 id应 ignore公司/部门上下文清空
void testPreHandle_noCompanyId_ignore() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
boolean result = interceptor.preHandle(request, response, new Object());
assertTrue(result);
assertTrue(CompanyContextHolder.isIgnore());
assertNull(CompanyContextHolder.getCompanyId());
assertNull(DeptContextHolder.getDeptId());
}
@Test // 有公司无部门:写入公司,部门清空
void testPreHandle_companyOnly() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
LoginUser loginUser = new LoginUser();
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null));
request.addHeader("visit-company-id", "11");
boolean result = interceptor.preHandle(request, response, new Object());
assertTrue(result);
assertFalse(CompanyContextHolder.isIgnore());
assertEquals(11L, CompanyContextHolder.getCompanyId());
assertFalse(DeptContextHolder.shouldIgnore());
assertNull(DeptContextHolder.getDeptId());
assertEquals(11L, loginUser.getVisitCompanyId());
assertNull(loginUser.getVisitDeptId());
}
@Test // 有公司+部门写入公司、部门上下文afterCompletion 清理 visitDeptId & holder
void testPreHandle_withCompanyAndDept_andAfterCompletionClear() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
LoginUser loginUser = new LoginUser();
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser, null));
request.addHeader("visit-company-id", "22");
request.addHeader("visit-dept-id", "33");
boolean result = interceptor.preHandle(request, response, new Object());
assertTrue(result);
assertFalse(CompanyContextHolder.isIgnore());
assertEquals(22L, CompanyContextHolder.getCompanyId());
assertEquals(33L, DeptContextHolder.getDeptId());
assertEquals(22L, DeptContextHolder.getCompanyId());
assertEquals(22L, loginUser.getVisitCompanyId());
assertEquals(33L, loginUser.getVisitDeptId());
// afterCompletion: 清理 visitCompanyId/visitDeptId 与 holder
interceptor.afterCompletion(request, response, new Object(), null);
assertEquals(0L, loginUser.getVisitCompanyId());
assertEquals(0L, loginUser.getVisitDeptId());
assertNull(DeptContextHolder.getDeptId());
assertNull(DeptContextHolder.getCompanyId());
}
}

View File

@@ -33,6 +33,7 @@ import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException; import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -286,8 +287,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
try { try {
boolean valid = CryptoSignatureUtils.verifySignature(signaturePayload, signatureType); boolean valid = CryptoSignatureUtils.verifySignature(signaturePayload, signatureType);
if (!valid) { if (!valid) {
log.error("[API-PORTAL] 签名校验失败"); throw new SecurityValidationException(HttpStatus.UNAUTHORIZED, "签名校验失败");
return;
} }
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "签名算法配置异常"); throw new SecurityValidationException(HttpStatus.INTERNAL_SERVER_ERROR, "签名算法配置异常");
@@ -305,15 +305,28 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
.build() .build()
.getQueryParams(); .getQueryParams();
params.forEach((key, values) -> { params.forEach((key, values) -> {
if (!StringUtils.hasText(key) || "signature".equalsIgnoreCase(key)) { String decodedKey = URLDecoder.decode(key, StandardCharsets.UTF_8);
if (!StringUtils.hasText(decodedKey) || "signature".equalsIgnoreCase(decodedKey)) {
return; return;
} }
if (CollectionUtils.isEmpty(values)) { if (CollectionUtils.isEmpty(values)) {
target.put(key, ""); target.put(decodedKey, "");
} else if (values.size() == 1) { return;
target.put(key, values.get(0)); }
// 对每一个 value 做 URL 解码,确保与客户端原文签名一致
List<String> decodedValues = values.stream()
.map(val -> URLDecoder.decode(val, StandardCharsets.UTF_8))
.toList();
boolean allNullLiteral = decodedValues.stream()
.allMatch(v -> "null".equals(v));
if (allNullLiteral) {
// 过滤掉仅包含字符串 "null" 的参数
return;
}
if (decodedValues.size() == 1) {
target.put(decodedKey, decodedValues.get(0));
} else { } else {
target.put(key, String.join(",", values)); target.put(decodedKey, String.join(",", decodedValues));
} }
}); });
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {

View File

@@ -5,6 +5,10 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; import com.zt.plat.framework.common.util.security.CryptoSignatureUtils;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.net.URI; import java.net.URI;
@@ -23,10 +27,6 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/** /**
* 可直接运行的示例,演示如何使用 appId=test 与对应密钥调用本地 Databus API。 * 可直接运行的示例,演示如何使用 appId=test 与对应密钥调用本地 Databus API。
@@ -37,14 +37,14 @@ public final class DatabusApiInvocationExample {
// private static final String APP_ID = "iwork"; // private static final String APP_ID = "iwork";
// private static final String APP_SECRET = "lpGXiNe/GMLk0vsbYGLa8eYxXq8tGhTbuu3/D4MJzIk="; // private static final String APP_SECRET = "lpGXiNe/GMLk0vsbYGLa8eYxXq8tGhTbuu3/D4MJzIk=";
private static final String APP_ID = "ztmy"; private static final String APP_ID = "jwyw";
private static final String APP_SECRET = "zFre/nTRGi7LpoFjN7oQkKeOT09x1fWTyIswrc702QQ="; private static final String APP_SECRET = "MhfCcqB59rDTnB5yGOVXWtp/5a0JXir7pSjPl5cVMJ8=";
private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES;
// private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1"; // private static final String TARGET_API = "http://172.16.46.63:30081/admin-api/databus/api/portal/callback/v1";
// private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; // private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1";
// private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; // private static final String TARGET_API = "http://172.16.46.195:48080/admin-api/databus/api/portal/lgstOpenApi/v1";
private static final String TARGET_API = "https://jygk.chncopper.com:30078/admin-api/databus/api/portal/lgstOpenApi/v1"; // private static final String TARGET_API = "https://jygk.chncopper.com:30078/admin-api/databus/api/portal/lgstOpenApi/v1";
// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/callback/v1"; private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456";
// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/lgstOpenApi/v1"; // private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/lgstOpenApi/v1";
// private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456"; // private static final String TARGET_API = "http://localhost:48080/admin-api/databus/api/portal/testcbw/456";
// ⚠️ 仅用于联调:信任所有证书 + 关闭主机名校验,生产环境请改为受信 CA 或自定义 truststore。 // ⚠️ 仅用于联调:信任所有证书 + 关闭主机名校验,生产环境请改为受信 CA 或自定义 truststore。
@@ -102,10 +102,16 @@ public final class DatabusApiInvocationExample {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
OUT.println("=== GET 请求示例 ==="); OUT.println("=== GET 请求示例 ===");
// executeGetExample(); executeGetExample();
// OUT.println(); // OUT.println();
// OUT.println("=== POST 请求示例 ==="); OUT.println("=== POST 请求示例 ===");
executePostExample(); executePostExample("""
{"operateFlag":"I","__interfaceType__":"R_MY_JY_03","data":{"endAddressName":"1","customerCompanyName":"中铜国贸","endAddressDetail":"测试地址","remark":" ","custSuppType":"1","shipperCompanyName":"中铜国贸","consigneeCorpCode":" ","consignerContactPhone":" 11","importFlag":"10","businessSupplierCode":" ","entrustMainCode":"WT3162251027027","endAddressCode":" ","specifyCarrierCorpCode":"10086689","materDetail":[{"detailStatus":"10","batchNo":"ZLTD2510ZTGM0017001","measureCodeMdm":"CU032110001","packType":" ","quantityPlanDetail":1,"deliveryOrderNo":"ZLTD2510ZTGM0017001","measureCode":"CU032110001","goodsSpecification":" ","measureUnitCode":"PAC","entrustDetailCode":"WT3162251027027001","brand":" ","soNumber":"68ecf0055502d565d22b378a"}],"operateFlag":1,"custSuppName":"上海锦生金属有限公司","startAddressCode":" ","planStartTime":1761556166000,"customerCompanyCode":0,"importMethod":"EXW","startAddressType":"10","shipperCompanyCode":"3162","deliverCondition":"20","businessSupplierName":" ","startAddressDetail":" 111","transType":"30","endAddressType":"20","planEndTime":1761556166000,"specifyCarrierCorpName":null,"custSuppFlag":"0101","businessType":"20","consigneeCorpName":" ","custSuppCode":"10086689","startAddressName":" 111","consignerContactName":" 11"},"datetime":"20251027170929","busiBillCode":"WT3162251027027","system":"BRMS","__requestId__":"f918841c-14fb-49eb-9640-c5d1b3d46bd1"}
""");
executePostExample("""
{"msgCode":"YWJYGK0003","data":"{\\"memberId\\":65352,\\"routes\\":[{\\"carrierCorpCode\\":\\"10193776\\",\\"carrierCorpName\\":\\"成都达海金属加工配送有限公司\\",\\"endAddressCode\\":\\"440000-440300\\",\\"endAddressDetail\\":\\"深圳港\\",\\"endAddressDetailDesc\\":\\"广东省深圳市盐田区深盐路\\",\\"endAddressLatitude\\":22.567426,\\"endAddressLongitude\\":114.283271,\\"endAddressName\\":\\"广东省-深圳市\\",\\"endAddressType\\":\\"port\\",\\"startAddressCode\\":\\"520000-0\\",\\"startAddressDetail\\":\\"安龙\\",\\"startAddressDetailDesc\\":\\"贵州省安龙县德卧镇坡告村\\",\\"startAddressLatitude\\":25.066532,\\"startAddressLongitude\\":105.244186,\\"startAddressName\\":\\"贵州省-null\\",\\"startAddressType\\":\\"railway-station\\",\\"taskEndTime\\":1766592000000,\\"taskStartTime\\":1766332800000,\\"transType\\":\\"10\\"},{\\"carrierCorpCode\\":\\"10193776\\",\\"carrierCorpName\\":\\"成都达海金属加工配送有限公司\\",\\"endAddressCode\\":\\"230000-230600\\",\\"endAddressDetail\\":\\"大庆东\\",\\"endAddressDetailDesc\\":\\"黑龙江省大庆市龙凤区凤一路28号\\",\\"endAddressLatitude\\":46.544097,\\"endAddressLongitude\\":125.118902,\\"endAddressName\\":\\"黑龙江省-大庆市\\",\\"endAddressType\\":\\"railway-station\\",\\"startAddressCode\\":\\"440000-440300\\",\\"startAddressDetail\\":\\"深圳港\\",\\"startAddressDetailDesc\\":\\"广东省深圳市盐田区深盐路\\",\\"startAddressLatitude\\":22.567426,\\"startAddressLongitude\\":114.283271,\\"startAddressName\\":\\"广东省-深圳市\\",\\"startAddressType\\":\\"port\\",\\"taskEndTime\\":1767110400000,\\"taskStartTime\\":1766592000000,\\"transType\\":\\"30\\"},{\\"carrierCorpCode\\":\\"10193776\\",\\"carrierCorpName\\":\\"成都达海金属加工配送有限公司\\",\\"endAddressCode\\":\\"520000-0\\",\\"endAddressDetail\\":\\"郑屯\\",\\"endAddressDetailDesc\\":\\"贵州省郑屯镇\\",\\"endAddressName\\":\\"贵州省-null\\",\\"endAddressType\\":\\"railway-station\\",\\"startAddressCode\\":\\"230000-230600\\",\\"startAddressDetail\\":\\"大庆东\\",\\"startAddressDetailDesc\\":\\"黑龙江省大庆市龙凤区凤一路28号\\",\\"startAddressLatitude\\":46.544097,\\"startAddressLongitude\\":125.118902,\\"startAddressName\\":\\"黑龙江省-大庆市\\",\\"startAddressType\\":\\"railway-station\\",\\"taskEndTime\\":1768320000000,\\"taskStartTime\\":1767110400000,\\"transType\\":\\"20\\"}],\\"taskLineNumber\\":\\"CT202512230001_001\\",\\"taskNumber\\":\\"CT202512230001\\"}"}
""");
} }
private static void executeGetExample() throws Exception { private static void executeGetExample() throws Exception {
@@ -113,9 +119,11 @@ public final class DatabusApiInvocationExample {
queryParams.put("businessCode", "11"); queryParams.put("businessCode", "11");
queryParams.put("fileId", "11"); queryParams.put("fileId", "11");
queryParams.put("null", null); queryParams.put("null", null);
queryParams.put("empty", "");
queryParams.put("taskTimeEnd", "2025-12-28 23:00:00");
String signature = generateSignature(queryParams, Map.of()); String signature = generateSignature(queryParams, Map.of());
URI requestUri = buildUri(TARGET_API, queryParams); URI requestUri = buildUri(TARGET_API, queryParams);
String nonce = "171615676c7d4d96b9f55f3d90ad27e0"; String nonce = randomNonce();
HttpRequest request = HttpRequest.newBuilder(requestUri) HttpRequest request = HttpRequest.newBuilder(requestUri)
.timeout(Duration.ofSeconds(10)) .timeout(Duration.ofSeconds(10))
@@ -131,16 +139,14 @@ public final class DatabusApiInvocationExample {
printResponse(response); printResponse(response);
} }
private static void executePostExample() throws Exception { private static void executePostExample(String json) throws Exception {
Map<String, Object> queryParams = new LinkedHashMap<>(); Map<String, Object> queryParams = new LinkedHashMap<>();
long extraTimestamp = 1761556157185L; long extraTimestamp = 1761556157185L;
// String bodyJson = String.format(""" // String bodyJson = String.json("""
// {"operateFlag":"I","__interfaceType__":"R_MY_JY_03","data":{"endAddressName":"1","customerCompanyName":"中铜国贸","endAddressDetail":"测试地址","remark":" ","custSuppType":"1","shipperCompanyName":"中铜国贸","consigneeCorpCode":" ","consignerContactPhone":" 11","importFlag":"10","businessSupplierCode":" ","entrustMainCode":"WT3162251027027","endAddressCode":" ","specifyCarrierCorpCode":"10086689","materDetail":[{"detailStatus":"10","batchNo":"ZLTD2510ZTGM0017001","measureCodeMdm":"CU032110001","packType":" ","quantityPlanDetail":1,"deliveryOrderNo":"ZLTD2510ZTGM0017001","measureCode":"CU032110001","goodsSpecification":" ","measureUnitCode":"PAC","entrustDetailCode":"WT3162251027027001","brand":" ","soNumber":"68ecf0055502d565d22b378a"}],"operateFlag":1,"custSuppName":"上海锦生金属有限公司","startAddressCode":" ","planStartTime":1761556166000,"customerCompanyCode":0,"importMethod":"EXW","startAddressType":"10","shipperCompanyCode":"3162","deliverCondition":"20","businessSupplierName":" ","startAddressDetail":" 111","transType":"30","endAddressType":"20","planEndTime":1761556166000,"specifyCarrierCorpName":null,"custSuppFlag":"0101","businessType":"20","consigneeCorpName":" ","custSuppCode":"10086689","startAddressName":" 111","consignerContactName":" 11"},"datetime":"20251027170929","busiBillCode":"WT3162251027027","system":"BRMS","__requestId__":"f918841c-14fb-49eb-9640-c5d1b3d46bd1"} // {"operateFlag":"I","__interfaceType__":"R_MY_JY_03","data":{"endAddressName":"1","customerCompanyName":"中铜国贸","endAddressDetail":"测试地址","remark":" ","custSuppType":"1","shipperCompanyName":"中铜国贸","consigneeCorpCode":" ","consignerContactPhone":" 11","importFlag":"10","businessSupplierCode":" ","entrustMainCode":"WT3162251027027","endAddressCode":" ","specifyCarrierCorpCode":"10086689","materDetail":[{"detailStatus":"10","batchNo":"ZLTD2510ZTGM0017001","measureCodeMdm":"CU032110001","packType":" ","quantityPlanDetail":1,"deliveryOrderNo":"ZLTD2510ZTGM0017001","measureCode":"CU032110001","goodsSpecification":" ","measureUnitCode":"PAC","entrustDetailCode":"WT3162251027027001","brand":" ","soNumber":"68ecf0055502d565d22b378a"}],"operateFlag":1,"custSuppName":"上海锦生金属有限公司","startAddressCode":" ","planStartTime":1761556166000,"customerCompanyCode":0,"importMethod":"EXW","startAddressType":"10","shipperCompanyCode":"3162","deliverCondition":"20","businessSupplierName":" ","startAddressDetail":" 111","transType":"30","endAddressType":"20","planEndTime":1761556166000,"specifyCarrierCorpName":null,"custSuppFlag":"0101","businessType":"20","consigneeCorpName":" ","custSuppCode":"10086689","startAddressName":" 111","consignerContactName":" 11"},"datetime":"20251027170929","busiBillCode":"WT3162251027027","system":"BRMS","__requestId__":"f918841c-14fb-49eb-9640-c5d1b3d46bd1"}
// """, extraTimestamp); // """, extraTimestamp);
String bodyJson = String.format(""" String bodyJson = String.format(json, extraTimestamp);
{}
""", extraTimestamp);
Map<String, Object> bodyParams = parseBodyJson(bodyJson); Map<String, Object> bodyParams = parseBodyJson(bodyJson);
String signature = generateSignature(queryParams, bodyParams); String signature = generateSignature(queryParams, bodyParams);

View File

@@ -2,6 +2,8 @@ package com.zt.plat.module.infra.service.file;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.zt.plat.framework.common.pojo.PageResult; import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.json.JsonUtils; import com.zt.plat.framework.common.util.json.JsonUtils;
import com.zt.plat.framework.common.util.validation.ValidationUtils; import com.zt.plat.framework.common.util.validation.ValidationUtils;
@@ -14,8 +16,6 @@ import com.zt.plat.module.infra.framework.file.core.client.FileClient;
import com.zt.plat.module.infra.framework.file.core.client.FileClientConfig; import com.zt.plat.module.infra.framework.file.core.client.FileClientConfig;
import com.zt.plat.module.infra.framework.file.core.client.FileClientFactory; import com.zt.plat.module.infra.framework.file.core.client.FileClientFactory;
import com.zt.plat.module.infra.framework.file.core.enums.FileStorageEnum; import com.zt.plat.module.infra.framework.file.core.enums.FileStorageEnum;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.Validator; import jakarta.validation.Validator;
import lombok.Getter; import lombok.Getter;
@@ -172,7 +172,7 @@ public class FileConfigServiceImpl implements FileConfigService {
// 校验存在 // 校验存在
validateFileConfigExists(id); validateFileConfigExists(id);
// 上传文件 // 上传文件
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg"); return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -41,7 +41,7 @@ public class FtpFileClientTest {
client.init(); client.init();
// 上传文件 // 上传文件
String path = IdUtil.fastSimpleUUID() + ".jpg"; String path = IdUtil.fastSimpleUUID() + ".jpg";
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
String fullPath = client.upload(content, path, "image/jpeg"); String fullPath = client.upload(content, path, "image/jpeg");
System.out.println("访问地址:" + fullPath); System.out.println("访问地址:" + fullPath);
if (false) { if (false) {

View File

@@ -20,7 +20,7 @@ public class LocalFileClientTest {
client.init(); client.init();
// 上传文件 // 上传文件
String path = IdUtil.fastSimpleUUID() + ".jpg"; String path = IdUtil.fastSimpleUUID() + ".jpg";
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
String fullPath = client.upload(content, path, "image/jpeg"); String fullPath = client.upload(content, path, "image/jpeg");
System.out.println("访问地址:" + fullPath); System.out.println("访问地址:" + fullPath);
client.delete(path); client.delete(path);

View File

@@ -101,7 +101,7 @@ public class S3FileClientTest {
client.init(); client.init();
// 上传文件 // 上传文件
String path = IdUtil.fastSimpleUUID() + ".jpg"; String path = IdUtil.fastSimpleUUID() + ".jpg";
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
String fullPath = client.upload(content, path, "image/jpeg"); String fullPath = client.upload(content, path, "image/jpeg");
System.out.println("访问地址:" + fullPath); System.out.println("访问地址:" + fullPath);
// 读取文件 // 读取文件

View File

@@ -34,7 +34,7 @@ public class SftpFileClientTest {
client.init(); client.init();
// 上传文件 // 上传文件
String path = IdUtil.fastSimpleUUID() + ".jpg"; String path = IdUtil.fastSimpleUUID() + ".jpg";
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
String fullPath = client.upload(content, path, "image/jpeg"); String fullPath = client.upload(content, path, "image/jpeg");
System.out.println("访问地址:" + fullPath); System.out.println("访问地址:" + fullPath);
if (false) { if (false) {

View File

@@ -88,7 +88,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testCreateFile_success_01() throws Exception { public void testCreateFile_success_01() throws Exception {
// 准备参数 // 准备参数
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
String name = "单测文件名"; String name = "单测文件名";
String directory = randomString(); String directory = randomString();
String type = "image/jpeg"; String type = "image/jpeg";
@@ -122,7 +122,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testCreateFile_success_02() throws Exception { public void testCreateFile_success_02() throws Exception {
// 准备参数 // 准备参数
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
// mock Master 文件客户端 // mock Master 文件客户端
String type = "image/jpeg"; String type = "image/jpeg";
FileClient client = mock(FileClient.class); FileClient client = mock(FileClient.class);
@@ -318,7 +318,7 @@ public class FileServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testCreateFile_withSameHash() throws Exception { public void testCreateFile_withSameHash() throws Exception {
// 准备参数 // 准备参数
byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); byte[] content = ResourceUtil.readBytes("file/bg1.png");
String name = "单测文件名"; String name = "单测文件名";
String directory = randomString(); String directory = randomString();
String type = "image/jpeg"; String type = "image/jpeg";

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.framework.common.pojo.CommonResult;
import com.zt.plat.module.system.api.permission.dto.*; import com.zt.plat.module.system.api.permission.dto.*;
import com.zt.plat.module.system.enums.ApiConstants; 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.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -50,4 +51,9 @@ public interface PermissionApi extends PermissionCommonApi {
@Parameter(name = "userId", description = "用户编号", example = "1", required = true) @Parameter(name = "userId", description = "用户编号", example = "1", required = true)
CommonResult<Set<Long>> getUserRoleIdListByUserId(@RequestParam("userId") Long userId); 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_NOT_FOUND = new ErrorCode(1_002_014_000, "验证码不存在");
ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期"); ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1_002_014_001, "验证码已过期");
ErrorCode SMS_CODE_USED = new ErrorCode(1_002_014_002, "验证码已使用"); 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_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_SEND_TOO_FAST = new ErrorCode(1_002_014_005, "短信发送过于频繁,请于{}分钟后再试");
// ========== 租户信息 1-002-015-000 ========== // ========== 租户信息 1-002-015-000 ==========
ErrorCode TENANT_NOT_EXISTS = new ErrorCode(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; package com.zt.plat.module.system.enums.permission;
import com.fasterxml.jackson.annotation.JsonValue;
import com.zt.plat.framework.common.core.ArrayValuable; import com.zt.plat.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import java.util.Arrays; 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); 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 @Override
public Integer[] array() { public Integer[] array() {
return ARRAYS; 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.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.PermissionAssignRoleDataScopeReqVO;
import com.zt.plat.module.system.controller.admin.permission.vo.permission.PermissionAssignUserRoleReqVO; 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 com.zt.plat.module.system.service.permission.PermissionService;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@@ -65,6 +66,11 @@ public class PermissionApiImpl implements PermissionApi {
return success(permissionService.getUserRoleIdListByUserIdFromCache(userId)); return success(permissionService.getUserRoleIdListByUserIdFromCache(userId));
} }
@Override
public CommonResult<DataScopeEnum> getUserDataPermissionLevel(Long userId) {
return success(permissionService.getUserDataPermissionLevel(userId));
}
@Override @Override
public CommonResult<Boolean> hasAnyPermissions(Long userId, String... permissions) { public CommonResult<Boolean> hasAnyPermissions(Long userId, String... permissions) {
return success(permissionService.hasAnyPermissions(userId, permissions)); return success(permissionService.hasAnyPermissions(userId, permissions));

View File

@@ -76,4 +76,7 @@ public class DeptSaveReqVO {
@Schema(description = "部门来源类型", example = "1") @Schema(description = "部门来源类型", example = "1")
private Integer deptSource; 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"}) @GetMapping({"/list-all-simple", "/simple-list"})
@Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项") @Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项")
public CommonResult<List<UserSimpleRespVO>> getSimpleUserList() { public CommonResult<List<UserSimpleRespVO>> getSimpleUserList(
List<AdminUserDO> list = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus(), SIMPLE_LIST_LIMIT); @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)); return success(UserConvert.INSTANCE.convertSimpleList(list));
} }

View File

@@ -23,6 +23,9 @@ public class UserPageReqVO extends PageParam {
@Schema(description = "用户账号,模糊匹配", example = "zt") @Schema(description = "用户账号,模糊匹配", example = "zt")
private String username; private String username;
@Schema(description = "用户昵称,模糊匹配", example = "张三")
private String nickname;
@Schema(description = "工号,模糊匹配", example = "A00123") @Schema(description = "工号,模糊匹配", example = "A00123")
private String workcode; private String workcode;

View File

@@ -114,12 +114,15 @@ public interface DeptMapper extends BaseMapperX<DeptDO> {
* @param parentId 父部门ID * @param parentId 父部门ID
* @return 编码最大的子部门 * @return 编码最大的子部门
*/ */
default DeptDO selectLastChildByCode(Long parentId) { default DeptDO selectLastChildByCode(Long parentId, String prefix) {
return selectOne(new LambdaQueryWrapper<DeptDO>() LambdaQueryWrapper<DeptDO> wrapper = new LambdaQueryWrapper<DeptDO>()
.eq(DeptDO::getParentId, parentId) .eq(DeptDO::getParentId, parentId)
.isNotNull(DeptDO::getCode) .isNotNull(DeptDO::getCode);
.orderByDesc(DeptDO::getCode) if (StrUtil.isNotBlank(prefix)) {
.last("LIMIT 1")); 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<>(); MPJLambdaWrapperX<AdminUserDO> query = new MPJLambdaWrapperX<>();
query.leftJoin(UserDeptDO.class, UserDeptDO::getUserId, AdminUserDO::getId); query.leftJoin(UserDeptDO.class, UserDeptDO::getUserId, AdminUserDO::getId);
query.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()); query.likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername());
query.likeIfPresent(AdminUserDO::getNickname, reqVO.getNickname());
query.likeIfPresent(AdminUserDO::getWorkcode, reqVO.getWorkcode()); query.likeIfPresent(AdminUserDO::getWorkcode, reqVO.getWorkcode());
query.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()); query.likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile());
query.eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()); 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)); 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>() LambdaQueryWrapperX<AdminUserDO> query = new LambdaQueryWrapperX<AdminUserDO>()
.eq(AdminUserDO::getStatus, status); .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) { if (limit != null && limit > 0) {
query.last("LIMIT " + limit); 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.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.google.common.annotations.VisibleForTesting; 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.enums.CommonStatusEnum;
import com.zt.plat.framework.common.pojo.CompanyDeptInfo; import com.zt.plat.framework.common.pojo.CompanyDeptInfo;
import com.zt.plat.framework.common.util.object.BeanUtils; 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.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.DeptListReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; 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.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.dataobject.userdept.UserDeptDO;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; 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.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.dal.redis.RedisKeyConstants;
import com.zt.plat.module.system.enums.dept.DeptSourceEnum; import com.zt.plat.module.system.enums.dept.DeptSourceEnum;
import com.zt.plat.module.system.enums.DictTypeConstants; import com.zt.plat.module.system.service.permission.PermissionService;
import com.zt.plat.module.system.service.dict.DictDataService;
import com.zt.plat.module.system.service.dict.DictTypeService;
import org.apache.seata.spring.annotation.GlobalTransactional; import org.apache.seata.spring.annotation.GlobalTransactional;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -57,17 +54,17 @@ public class DeptServiceImpl implements DeptService {
@Resource @Resource
private UserDeptMapper userDeptMapper; private UserDeptMapper userDeptMapper;
@Resource @Resource
private PermissionService permissionService;
@Resource
private com.zt.plat.module.system.mq.producer.databus.DatabusChangeProducer databusChangeProducer; private com.zt.plat.module.system.mq.producer.databus.DatabusChangeProducer databusChangeProducer;
@Resource @Resource
private DeptExternalCodeService deptExternalCodeService; private DeptExternalCodeService deptExternalCodeService;
@Resource
private DictTypeService dictTypeService;
@Resource
private DictDataService dictDataService;
private static final String ROOT_CODE_PREFIX = "ZT"; 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 CODE_SEGMENT_LENGTH = 3;
private static final int MAX_SEQUENCE = 999; private static final int MAX_SEQUENCE = 999;
private static final int BATCH_SIZE = 1000;
private static final Comparator<DeptDO> DEPT_COMPARATOR = Comparator private static final Comparator<DeptDO> DEPT_COMPARATOR = Comparator
.comparing(DeptDO::getSort, Comparator.nullsLast(Comparator.naturalOrder())) .comparing(DeptDO::getSort, Comparator.nullsLast(Comparator.naturalOrder()))
.thenComparing(DeptDO::getId, 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.setParentId(normalizeParentId(createReqVO.getParentId()));
// 创建时默认有效 // 创建时默认有效
createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); createReqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
// 默认部门来源:未指定时视为外部部门
if (createReqVO.getDeptSource() == null) {
createReqVO.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
}
// 校验父部门的有效性 // 校验父部门的有效性
validateParentDept(null, createReqVO.getParentId()); validateParentDept(null, createReqVO.getParentId());
// 校验部门名的唯一性 // 校验部门名的唯一性
validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName()); validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
// 生成并校验部门编码 // 生成并校验部门编码所有来源统一走生成逻辑iWork 不再豁免)
Long effectiveParentId = normalizeParentId(createReqVO.getParentId()); if (Boolean.TRUE.equals(createReqVO.getDelayCodeGeneration())) {
String resolvedCode = generateDeptCode(effectiveParentId); createReqVO.setCode(null);
validateDeptCodeUnique(null, resolvedCode); } else {
createReqVO.setCode(resolvedCode); String resolvedCode = generateDeptCode(createReqVO.getParentId(), createReqVO.getDeptSource());
validateDeptCodeUnique(null, resolvedCode);
createReqVO.setCode(resolvedCode);
}
// 插入部门 // 插入部门
DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class); DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class);
// 设置部门来源:如果未指定,默认为外部部门 // 设置部门来源(前置已默认化,此处兜底)
if (dept.getDeptSource() == null) { if (dept.getDeptSource() == null) {
dept.setDeptSource(DeptSourceEnum.EXTERNAL.getSource()); dept.setDeptSource(DeptSourceEnum.EXTERNAL.getSource());
} }
deptMapper.insert(dept); deptMapper.insert(dept);
// 维护外部系统编码映射(若有传入) // 外部编码映射
upsertExternalCodeMapping(createReqVO, dept.getId()); upsertExternalMappingIfPresent(dept.getId(), createReqVO);
// 发布部门创建事件 // 发布部门创建事件
databusChangeProducer.sendDeptCreatedMessage(dept); databusChangeProducer.sendDeptCreatedMessage(dept);
@@ -109,6 +113,15 @@ public class DeptServiceImpl implements DeptService {
return dept.getId(); 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 @Override
@CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST,
allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存
@@ -122,34 +135,36 @@ public class DeptServiceImpl implements DeptService {
validateParentDept(updateReqVO.getId(), updateReqVO.getParentId()); validateParentDept(updateReqVO.getId(), updateReqVO.getParentId());
// 校验部门名的唯一性 // 校验部门名的唯一性
validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName()); validateDeptNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
Long newParentId = normalizeParentId(updateReqVO.getParentId()); Integer source = ObjectUtil.defaultIfNull(updateReqVO.getDeptSource(), originalDept.getDeptSource());
Long oldParentId = normalizeParentId(originalDept.getParentId()); if (source == null) {
boolean parentChanged = !Objects.equals(newParentId, oldParentId); source = DeptSourceEnum.EXTERNAL.getSource();
String existingCode = originalDept.getCode(); }
boolean needRegenerateCode = StrUtil.isBlank(existingCode); String existingCode = originalDept.getCode();
String resolvedCode = existingCode; if (StrUtil.isBlank(existingCode)) {
if (needRegenerateCode) { if (Boolean.TRUE.equals(updateReqVO.getDelayCodeGeneration())) {
resolvedCode = generateDeptCode(newParentId); updateReqVO.setCode(null);
validateDeptCodeUnique(updateReqVO.getId(), resolvedCode); } 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); DeptDO updateObj = BeanUtils.toBean(updateReqVO, DeptDO.class);
deptMapper.updateById(updateObj); deptMapper.updateById(updateObj);
// 外部编码映射
upsertExternalMappingIfPresent(updateObj.getId(), updateReqVO);
// 发布部门更新事件(重新查询获取完整数据) // 发布部门更新事件(重新查询获取完整数据)
DeptDO updatedDept = deptMapper.selectById(updateObj.getId()); DeptDO updatedDept = deptMapper.selectById(updateObj.getId());
if (updatedDept != null) { if (updatedDept != null) {
databusChangeProducer.sendDeptUpdatedMessage(updatedDept); databusChangeProducer.sendDeptUpdatedMessage(updatedDept);
} }
if (needRegenerateCode) {
refreshChildCodesRecursively(updateObj.getId(), updateReqVO.getCode());
}
// 维护外部系统编码映射(若有传入)
upsertExternalCodeMapping(updateReqVO, updateReqVO.getId());
} }
@Override @Override
@@ -167,7 +182,7 @@ public class DeptServiceImpl implements DeptService {
DeptDO dept = deptMapper.selectById(id); DeptDO dept = deptMapper.selectById(id);
Long tenantId = (dept != null) ? dept.getTenantId() : null; Long tenantId = (dept != null) ? dept.getTenantId() : null;
// 级联删除外部编码映射并清理缓存 // 级联删除外部编码映射
deptExternalCodeService.deleteDeptExternalCodesByDeptId(id); 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 effectiveParentId = normalizeParentId(parentId);
Long codeParentId = effectiveParentId; String prefix = resolveCodePrefix(effectiveParentId, deptSource);
String prefix = ROOT_CODE_PREFIX; int nextSequence = determineNextSequence(effectiveParentId, 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);
assertSequenceRange(nextSequence); assertSequenceRange(nextSequence);
return prefix + formatSequence(nextSequence); return prefix + formatSequence(nextSequence);
} }
private int determineNextSequence(Long parentId, String prefix) { 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); Integer sequence = parseSequence(lastChild != null ? lastChild.getCode() : null, prefix);
if (sequence != null) { if (sequence != null) {
return sequence + 1; return sequence + 1;
@@ -365,12 +370,36 @@ public class DeptServiceImpl implements DeptService {
candidate = candidate.trim(); candidate = candidate.trim();
} }
if (StrUtil.isBlank(candidate)) { if (StrUtil.isBlank(candidate)) {
candidate = generateDeptCode(DeptDO.PARENT_ID_ROOT); candidate = generateDeptCode(DeptDO.PARENT_ID_ROOT, DeptSourceEnum.EXTERNAL.getSource());
} }
validateDeptCodeUnique(currentDeptId, candidate); validateDeptCodeUnique(currentDeptId, candidate);
return 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 @Override
public DeptDO getDept(Long id) { public DeptDO getDept(Long id) {
return deptMapper.selectById(id); return deptMapper.selectById(id);
@@ -558,37 +587,59 @@ public class DeptServiceImpl implements DeptService {
@Override @Override
public List<DeptDO> getTopLevelDeptList() { public List<DeptDO> getTopLevelDeptList() {
// 获取当前用户所属的部门列表 Long loginUserId = getLoginUserId();
Set<Long> deptIds = userDeptMapper.selectValidListByUserIds(singleton(getLoginUserId()))
.stream() // 当前用户所属部门
.map(UserDeptDO::getDeptId) Set<Long> userDeptIds = Optional.ofNullable(userDeptMapper.selectValidListByUserIds(singleton(loginUserId)))
.collect(Collectors.toSet()); .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)) { if (CollUtil.isEmpty(deptIds)) {
// 如果用户没有关联任何部门,返回空列表
return Collections.emptyList(); return Collections.emptyList();
} }
// 获取用户所属部门的最顶层祖先部门 // 缓存已加载的部门,避免重复 IO
Set<Long> topLevelDeptIds = new HashSet<>(); Map<Long, DeptDO> deptCache = new HashMap<>();
for (Long deptId : deptIds) {
DeptDO dept = getDept(deptId); // 批量解析最顶层祖先(到 ROOT 或上级禁用即停),减少循环 IO
if (dept != null && CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) { Map<Long, Long> topLevelMap = findTopLevelAncestorIdsBatch(deptIds, deptCache);
// 找到该部门的最顶层祖先
DeptDO topLevelDept = findTopLevelAncestor(dept); // 汇总顶层部门 ID 并取实体(使用缓存避免再查)
if (topLevelDept != null) { Set<Long> topLevelDeptIds = topLevelMap.values().stream()
topLevelDeptIds.add(topLevelDept.getId()); .filter(Objects::nonNull)
} .collect(Collectors.toSet());
}
} List<DeptDO> topLevelDepts = topLevelDeptIds.stream()
.map(id -> deptCache.computeIfAbsent(id, this::getDept))
// 根据顶层部门ID获取部门详情 .filter(Objects::nonNull)
return topLevelDeptIds.stream() .filter(dept -> CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus()))
.map(this::getDept) .distinct()
.filter(Objects::nonNull) .collect(Collectors.toList());
.filter(dept -> CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus()))
.distinct() // 按 sortnullsLast再按 id 排序
.collect(Collectors.toList()); topLevelDepts.sort(DEPT_COMPARATOR);
return topLevelDepts;
} }
/** /**
@@ -741,64 +792,134 @@ public class DeptServiceImpl implements DeptService {
return dept; return dept;
} }
private void upsertExternalCodeMapping(DeptSaveReqVO reqVO, Long deptId) { /**
if (reqVO == null || deptId == null) { * 批量查找部门的最顶层祖先(到 ROOT 或遇到禁用/缺失的父部门即停止)
return; * 使用 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()); // 当前指针:原始部门 -> 当前向上追溯的部门 ID
if (StrUtil.isBlank(systemCode) || StrUtil.isBlank(externalCode)) { Map<Long, Long> cursorMap = new HashMap<>();
return; for (Long id : deptIds) {
cursorMap.put(id, id);
} }
// 缺失的外部系统字典类型或数据会自动补齐
ensureExternalSystemDict(systemCode); // 预先加载首批部门
deptExternalCodeService.saveOrUpdateDeptExternalCode( loadDeptBatch(cursorMap.values(), deptCache);
deptId,
systemCode, int safety = 0;
externalCode, while (!cursorMap.isEmpty() && safety++ < Short.MAX_VALUE) {
reqVO.getExternalDeptName(), // 收集本轮需要加载的父部门 ID避免重复加载
CommonStatusEnum.ENABLE.getStatus()); 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) { private void loadDeptBatch(Collection<Long> ids, Map<Long, DeptDO> deptCache) {
String normalizedCode = StrUtil.trimToNull(systemCode); if (CollUtil.isEmpty(ids)) {
if (normalizedCode == null) { return;
}
List<Long> toLoad = ids.stream()
.filter(Objects::nonNull)
.filter(id -> !deptCache.containsKey(id))
.distinct()
.collect(Collectors.toList());
if (CollUtil.isEmpty(toLoad)) {
return; 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) { for (int i = 0; i < toLoad.size(); i += BATCH_SIZE) {
DictDataSaveReqVO dataReq = new DictDataSaveReqVO(); int end = Math.min(i + BATCH_SIZE, toLoad.size());
dataReq.setDictType(DictTypeConstants.DEPT_EXTERNAL_SYSTEM); List<Long> batch = toLoad.subList(i, end);
dataReq.setLabel(normalizedCode); List<DeptDO> depts = getDeptList(batch);
dataReq.setValue(normalizedCode); if (CollUtil.isEmpty(depts)) {
dataReq.setSort(0); continue;
dataReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); }
dataReq.setRemark("外部组织同步自动创建"); for (DeptDO dept : depts) {
dictDataService.createDictData(dataReq); 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.IWorkHrSubcompanyPageRespVO;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO; import com.zt.plat.module.system.controller.admin.integration.iwork.vo.IWorkHrUserPageRespVO;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
/** /**
* Abstraction for applying iWork entities into local persistence. * Abstraction for applying iWork entities into local persistence.
*/ */
public interface IWorkSyncProcessor { 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);
BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data,
SyncOptions options,
DeptSyncContext context);
BatchResult syncJobTitles(List<IWorkHrJobTitlePageRespVO.JobTitle> data, SyncOptions options); BatchResult syncJobTitles(List<IWorkHrJobTitlePageRespVO.JobTitle> data, SyncOptions options);
BatchResult syncUsers(List<IWorkHrUserPageRespVO.User> 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. * 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. * Aggregated result for a sync batch.
*/ */
@@ -170,11 +215,11 @@ public interface IWorkSyncProcessor {
} }
default BatchResult syncSubcompany(IWorkHrSubcompanyPageRespVO.Subcompany data, SyncOptions options) { 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) { 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) { default BatchResult syncJobTitle(IWorkHrJobTitlePageRespVO.JobTitle data, SyncOptions options) {

View File

@@ -50,13 +50,34 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
@Override @Override
public BatchResult syncSubcompanies(List<IWorkHrSubcompanyPageRespVO.Subcompany> data, SyncOptions options) { 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); List<IWorkHrSubcompanyPageRespVO.Subcompany> records = CollUtil.emptyIfNull(data);
BatchResult result = BatchResult.empty(); BatchResult result = BatchResult.empty();
if (records.isEmpty()) { if (records.isEmpty()
&& (context == null || CollUtil.isEmpty(context.getPendingSubcompanies()))) {
return result; return result;
} }
result.increasePulled(records.size()); 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 guard = 0;
int maxPasses = Math.max(1, queue.size() * 2); int maxPasses = Math.max(1, queue.size() * 2);
while (!queue.isEmpty() && guard++ < maxPasses) { while (!queue.isEmpty() && guard++ < maxPasses) {
@@ -79,6 +100,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
} }
Long deptId = externalId.longValue(); Long deptId = externalId.longValue();
ParentHolder parentHolder = resolveSubcompanyParent(sub.getSupsubcomid()); ParentHolder parentHolder = resolveSubcompanyParent(sub.getSupsubcomid());
if (!isParentReady(parentHolder.parentId(), readyParentIds)) {
continue;
}
boolean canceled = isCanceledFlag(sub.getCanceled()); boolean canceled = isCanceledFlag(sub.getCanceled());
DeptSaveReqVO saveReq = buildSubcompanySaveReq(sub, deptId, parentHolder.parentId(), canceled); DeptSaveReqVO saveReq = buildSubcompanySaveReq(sub, deptId, parentHolder.parentId(), canceled);
try { try {
@@ -87,6 +111,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
canceled, canceled,
options); options);
applyDeptOutcome(result, outcome, "分部", sub.getSubcompanyname()); applyDeptOutcome(result, outcome, "分部", sub.getSubcompanyname());
if (outcome.deptId() != null) {
readyParentIds.add(outcome.deptId());
}
} catch (Exception ex) { } catch (Exception ex) {
log.error("[iWork] 同步分部失败: id={} name={}", sub.getId(), sub.getSubcompanyname(), ex); log.error("[iWork] 同步分部失败: id={} name={}", sub.getId(), sub.getSubcompanyname(), ex);
result.increaseFailed(); result.increaseFailed();
@@ -99,10 +126,30 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
break; break;
} }
} }
if (!queue.isEmpty()) {
if (context != null && !allowPlaceholderOnRemaining) {
context.getPendingSubcompanies().addAll(queue);
queue.clear();
}
}
if (!queue.isEmpty()) { if (!queue.isEmpty()) {
for (IWorkHrSubcompanyPageRespVO.Subcompany remaining : queue) { for (IWorkHrSubcompanyPageRespVO.Subcompany remaining : queue) {
log.warn("[iWork] 分部父级缺失未同步: id={} name={}", remaining.getId(), remaining.getSubcompanyname()); log.warn("[iWork] 分部父级缺失,延迟生成编码插入占位: id={} name={}", remaining.getId(), remaining.getSubcompanyname());
result.increaseFailed(); 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; return result;
@@ -110,13 +157,34 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
@Override @Override
public BatchResult syncDepartments(List<IWorkHrDepartmentPageRespVO.Department> data, SyncOptions options) { 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); List<IWorkHrDepartmentPageRespVO.Department> records = CollUtil.emptyIfNull(data);
BatchResult result = BatchResult.empty(); BatchResult result = BatchResult.empty();
if (records.isEmpty()) { if (records.isEmpty()
&& (context == null || CollUtil.isEmpty(context.getPendingDepartments()))) {
return result; return result;
} }
result.increasePulled(records.size()); 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 guard = 0;
int maxPasses = Math.max(1, queue.size() * 2); int maxPasses = Math.max(1, queue.size() * 2);
while (!queue.isEmpty() && guard++ < maxPasses) { while (!queue.isEmpty() && guard++ < maxPasses) {
@@ -139,6 +207,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
} }
Long deptId = externalId.longValue(); Long deptId = externalId.longValue();
ParentHolder parentHolder = resolveDepartmentParent(dept); ParentHolder parentHolder = resolveDepartmentParent(dept);
if (!isParentReady(parentHolder.parentId(), readyParentIds)) {
continue;
}
boolean canceled = isCanceledFlag(dept.getCanceled()); boolean canceled = isCanceledFlag(dept.getCanceled());
DeptSaveReqVO saveReq = buildDepartmentSaveReq(dept, deptId, parentHolder.parentId(), canceled); DeptSaveReqVO saveReq = buildDepartmentSaveReq(dept, deptId, parentHolder.parentId(), canceled);
try { try {
@@ -147,6 +218,9 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
canceled, canceled,
options); options);
applyDeptOutcome(result, outcome, "部门", dept.getDepartmentname()); applyDeptOutcome(result, outcome, "部门", dept.getDepartmentname());
if (outcome.deptId() != null) {
readyParentIds.add(outcome.deptId());
}
} catch (Exception ex) { } catch (Exception ex) {
log.error("[iWork] 同步部门失败: id={} name={}", dept.getId(), dept.getDepartmentname(), ex); log.error("[iWork] 同步部门失败: id={} name={}", dept.getId(), dept.getDepartmentname(), ex);
result.increaseFailed(); result.increaseFailed();
@@ -160,11 +234,42 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
} }
} }
if (!queue.isEmpty()) { if (!queue.isEmpty()) {
for (IWorkHrDepartmentPageRespVO.Department remaining : queue) { if (context != null && !allowPlaceholderOnRemaining) {
log.warn("[iWork] 部门因父级缺失未同步: id={} name={}", remaining.getId(), remaining.getDepartmentname()); context.getPendingDepartments().addAll(queue);
result.increaseFailed(); 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; return result;
} }
@@ -236,7 +341,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE; CommonStatusEnum status = inactive ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE;
// 直接沿用 iWork 原始密码,避免重复格式化造成校验偏差 // 直接沿用 iWork 原始密码,避免重复格式化造成校验偏差
String externalPassword = trimToNull(user.getPassword()); String externalPassword = trimToNull(user.getPassword());
AdminUserDO existing = adminUserMapper.selectByUsername(username); AdminUserDO existing = adminUserMapper.selectById(user.getId());
UserSyncOutcome outcome; UserSyncOutcome outcome;
if (existing == null) { if (existing == null) {
if (!options.isCreateIfMissing()) { if (!options.isCreateIfMissing()) {
@@ -408,7 +513,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
req.setIsGroup(Boolean.FALSE); req.setIsGroup(Boolean.FALSE);
req.setDeptSource(DeptSourceEnum.IWORK.getSource()); req.setDeptSource(DeptSourceEnum.IWORK.getSource());
req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode()); 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()); req.setExternalDeptName(data.getSubcompanyname());
return req; return req;
} }
@@ -428,7 +533,7 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
req.setIsGroup(Boolean.FALSE); req.setIsGroup(Boolean.FALSE);
req.setDeptSource(DeptSourceEnum.IWORK.getSource()); req.setDeptSource(DeptSourceEnum.IWORK.getSource());
req.setExternalSystemCode(ExternalPlatformEnum.IWORK.getCode()); 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()); req.setExternalDeptName(data.getDepartmentname());
return req; return req;
} }
@@ -493,6 +598,16 @@ public class IWorkSyncProcessorImpl implements IWorkSyncProcessor {
return new ParentHolder(DeptDO.PARENT_ID_ROOT); 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) { private PostDO resolvePostByCode(String code) {
String key = buildPostCacheKey(code); String key = buildPostCacheKey(code);
PostDO cached = postCache.get(key); 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.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.module.system.controller.admin.integration.iwork.vo.*; 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.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.IWorkOrgRestService;
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor; import com.zt.plat.module.system.service.integration.iwork.IWorkSyncProcessor;
import com.zt.plat.module.system.service.integration.iwork.IWorkSyncService; 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 IWorkOrgRestService orgRestService;
private final IWorkSyncProcessor syncProcessor; private final IWorkSyncProcessor syncProcessor;
private final DeptService deptService;
@Override @Override
public IWorkFullSyncRespVO fullSyncDepartments(IWorkFullSyncReqVO reqVO) { public IWorkFullSyncRespVO fullSyncDepartments(IWorkFullSyncReqVO reqVO) {
@@ -64,11 +66,14 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
boolean syncJobTitle = scopes.contains(IWorkSyncEntityTypeEnum.JOB_TITLE); boolean syncJobTitle = scopes.contains(IWorkSyncEntityTypeEnum.JOB_TITLE);
int processedPages = 0; int processedPages = 0;
IWorkSyncProcessor.SyncOptions options = buildFullSyncOptions(reqVO); IWorkSyncProcessor.SyncOptions options = buildFullSyncOptions(reqVO);
IWorkSyncProcessor.DeptSyncContext deptSyncContext = (syncDepartments || syncSubcompanies)
? new IWorkSyncProcessor.DeptSyncContext()
: null;
if (syncSubcompanies) { if (syncSubcompanies) {
processedPages += executeSubcompanyFullSync(reqVO, options, respVO.getSubcompanyStat(), batchStats); processedPages += executeSubcompanyFullSync(reqVO, options, respVO.getSubcompanyStat(), batchStats, deptSyncContext);
} }
if (syncDepartments) { if (syncDepartments) {
processedPages += executeDepartmentFullSync(reqVO, options, respVO.getDepartmentStat(), batchStats); processedPages += executeDepartmentFullSync(reqVO, options, respVO.getDepartmentStat(), batchStats, deptSyncContext);
} }
if (syncJobTitle) { if (syncJobTitle) {
processedPages += executeJobTitleFullSync(reqVO, options, respVO.getJobTitleStat(), batchStats); processedPages += executeJobTitleFullSync(reqVO, options, respVO.getJobTitleStat(), batchStats);
@@ -76,6 +81,13 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
if (syncUsers) { if (syncUsers) {
processedPages += executeUserFullSync(reqVO, options, respVO.getUserStat(), batchStats); 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); respVO.setProcessedPages(processedPages);
return respVO; return respVO;
} }
@@ -83,7 +95,8 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
private int executeSubcompanyFullSync(IWorkFullSyncReqVO reqVO, private int executeSubcompanyFullSync(IWorkFullSyncReqVO reqVO,
IWorkSyncProcessor.SyncOptions options, IWorkSyncProcessor.SyncOptions options,
IWorkSyncEntityStatVO stat, IWorkSyncEntityStatVO stat,
List<IWorkSyncBatchStatVO> batches) { List<IWorkSyncBatchStatVO> batches,
IWorkSyncProcessor.DeptSyncContext context) {
return executePaged(reqVO, IWorkSyncEntityTypeEnum.SUBCOMPANY, batches, (page, pageSize) -> { return executePaged(reqVO, IWorkSyncEntityTypeEnum.SUBCOMPANY, batches, (page, pageSize) -> {
IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO(); IWorkSubcompanyQueryReqVO query = new IWorkSubcompanyQueryReqVO();
query.setCurpage(page); query.setCurpage(page);
@@ -92,7 +105,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query); IWorkHrSubcompanyPageRespVO pageResp = orgRestService.listSubcompanies(query);
ensureIWorkSuccess("拉取分部", pageResp.isSuccess(), pageResp.getMessage()); ensureIWorkSuccess("拉取分部", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrSubcompanyPageRespVO.Subcompany> dataList = CollUtil.emptyIfNull(pageResp.getDataList()); 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()); updateStat(stat, result, dataList.size());
return new BatchExecution(result, dataList.size()); return new BatchExecution(result, dataList.size());
}); });
@@ -101,7 +114,8 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
private int executeDepartmentFullSync(IWorkFullSyncReqVO reqVO, private int executeDepartmentFullSync(IWorkFullSyncReqVO reqVO,
IWorkSyncProcessor.SyncOptions options, IWorkSyncProcessor.SyncOptions options,
IWorkSyncEntityStatVO stat, IWorkSyncEntityStatVO stat,
List<IWorkSyncBatchStatVO> batches) { List<IWorkSyncBatchStatVO> batches,
IWorkSyncProcessor.DeptSyncContext context) {
return executePaged(reqVO, IWorkSyncEntityTypeEnum.DEPARTMENT, batches, (page, pageSize) -> { return executePaged(reqVO, IWorkSyncEntityTypeEnum.DEPARTMENT, batches, (page, pageSize) -> {
IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO(); IWorkDepartmentQueryReqVO query = new IWorkDepartmentQueryReqVO();
query.setCurpage(page); query.setCurpage(page);
@@ -110,7 +124,7 @@ public class IWorkSyncServiceImpl implements IWorkSyncService {
IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query); IWorkHrDepartmentPageRespVO pageResp = orgRestService.listDepartments(query);
ensureIWorkSuccess("拉取部门", pageResp.isSuccess(), pageResp.getMessage()); ensureIWorkSuccess("拉取部门", pageResp.isSuccess(), pageResp.getMessage());
List<IWorkHrDepartmentPageRespVO.Department> dataList = CollUtil.emptyIfNull(pageResp.getDataList()); 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()); updateStat(stat, result, dataList.size());
return new BatchExecution(result, dataList.size()); return new BatchExecution(result, dataList.size());
}); });

View File

@@ -1,6 +1,7 @@
package com.zt.plat.module.system.service.permission; package com.zt.plat.module.system.service.permission;
import com.zt.plat.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO; 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.Collection;
import java.util.Set; import java.util.Set;
@@ -143,4 +144,12 @@ public interface PermissionService {
*/ */
DeptDataPermissionRespDTO getDeptDataPermission(Long userId); 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.dept.DeptService;
import com.zt.plat.module.system.service.user.AdminUserService; import com.zt.plat.module.system.service.user.AdminUserService;
import com.zt.plat.module.system.service.userdept.UserDeptService; import com.zt.plat.module.system.service.userdept.UserDeptService;
import com.zt.plat.framework.tenant.core.aop.TenantIgnore;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; 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 @Slf4j
public class PermissionServiceImpl implements PermissionService { 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 @Resource
private RoleMenuMapper roleMenuMapper; private RoleMenuMapper roleMenuMapper;
@Resource @Resource
@@ -404,6 +414,40 @@ public class PermissionServiceImpl implements PermissionService {
return result; 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 生效问题 * 获得自身的代理对象,解决 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.dal.mysql.sms.SmsCodeMapper;
import com.zt.plat.module.system.enums.sms.SmsSceneEnum; import com.zt.plat.module.system.enums.sms.SmsSceneEnum;
import com.zt.plat.module.system.framework.sms.config.SmsCodeProperties; import com.zt.plat.module.system.framework.sms.config.SmsCodeProperties;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import static cn.hutool.core.util.RandomUtil.randomInt; import static cn.hutool.core.util.RandomUtil.randomInt;
@@ -56,11 +56,11 @@ public class SmsCodeServiceImpl implements SmsCodeService {
if (lastSmsCode != null) { if (lastSmsCode != null) {
if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis() if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis()
< smsCodeProperties.getSendFrequency().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()) && // 必须是今天,才能计算超过当天的上限 if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限
lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。 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 每天可发送数量
// TODO ZT提升每个 IP 每小时可发送数量 // TODO ZT提升每个 IP 每小时可发送数量

View File

@@ -193,10 +193,14 @@ public interface AdminUserService {
* @param status 状态 * @param status 状态
* @return 用户们 * @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) { 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 @Override
public List<AdminUserDO> getUserListByStatus(Integer status, Integer limit) { public List<AdminUserDO> getUserListByStatus(Integer status, Integer limit, String keyword) {
List<AdminUserDO> users = userMapper.selectListByStatus(status, limit); List<AdminUserDO> users = userMapper.selectListByStatus(status, limit, keyword);
fillUserDeptInfo(users); fillUserDeptInfo(users);
return users; return users;
} }

View File

@@ -241,8 +241,8 @@ zt:
expire-times: 10m expire-times: 10m
send-frequency: 1m send-frequency: 1m
send-maximum-quantity-per-day: 10 send-maximum-quantity-per-day: 10
begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 begin-code: 100000
end-code: 9999 # 这里配置 9999 的原因是,测试方便。 end-code: 999999
# E办OAuth2配置文件 # 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.enums.CommonStatusEnum;
import com.zt.plat.framework.common.util.object.ObjectUtils; import com.zt.plat.framework.common.util.object.ObjectUtils;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest; 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.DeptListReqVO;
import com.zt.plat.module.system.controller.admin.dept.vo.dept.DeptSaveReqVO; 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.DeptDO;
import com.zt.plat.module.system.dal.dataobject.dept.DeptExternalCodeDO; 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.DeptExternalCodeMapper;
import com.zt.plat.module.system.dal.mysql.dept.DeptMapper; 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.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 jakarta.annotation.Resource;
import org.junit.jupiter.api.Test; 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.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.Bean;
import org.springframework.context.annotation.Import;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -51,6 +53,9 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Resource @Resource
private CacheManager cacheManager; private CacheManager cacheManager;
@MockBean
private PermissionService permissionService;
@TestConfiguration @TestConfiguration
@EnableCaching @EnableCaching
static class CacheConfig { static class CacheConfig {
@@ -69,7 +74,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
reqVO.setName(name); reqVO.setName(name);
reqVO.setSort(sort); reqVO.setSort(sort);
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
reqVO.setDeptSource(1); reqVO.setDeptSource(DeptSourceEnum.SYNC.getSource());
reqVO.setIsCompany(false); reqVO.setIsCompany(false);
reqVO.setIsGroup(false); reqVO.setIsGroup(false);
return deptService.createDept(reqVO); return deptService.createDept(reqVO);
@@ -83,7 +88,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setParentId(DeptDO.PARENT_ID_ROOT); o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setStatus(randomCommonStatus()); o.setStatus(randomCommonStatus());
o.setCode(null); o.setCode(null);
}).setDeptSource(1); }).setDeptSource(DeptSourceEnum.SYNC.getSource());
// 调用 // 调用
Long deptId = deptService.createDept(reqVO); Long deptId = deptService.createDept(reqVO);
@@ -105,7 +110,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
childReq.setName("事业部"); childReq.setName("事业部");
childReq.setSort(1); childReq.setSort(1);
childReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); childReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
childReq.setDeptSource(1); childReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long childId = deptService.createDept(childReq); Long childId = deptService.createDept(childReq);
DeptDO childDept = deptMapper.selectById(childId); DeptDO childDept = deptMapper.selectById(childId);
@@ -119,7 +124,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
topLevelReq.setName("总部"); topLevelReq.setName("总部");
topLevelReq.setSort(1); topLevelReq.setSort(1);
topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
topLevelReq.setDeptSource(1); topLevelReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long topLevelId = deptService.createDept(topLevelReq); Long topLevelId = deptService.createDept(topLevelReq);
DeptDO firstTop = deptMapper.selectById(topLevelId); DeptDO firstTop = deptMapper.selectById(topLevelId);
assertEquals("ZT001", firstTop.getCode()); assertEquals("ZT001", firstTop.getCode());
@@ -129,12 +134,185 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
secondTopLevelReq.setName("总部"); secondTopLevelReq.setName("总部");
secondTopLevelReq.setSort(2); secondTopLevelReq.setSort(2);
secondTopLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); secondTopLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
secondTopLevelReq.setDeptSource(1); secondTopLevelReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
Long secondTopId = deptService.createDept(secondTopLevelReq); Long secondTopId = deptService.createDept(secondTopLevelReq);
DeptDO secondTop = deptMapper.selectById(secondTopId); DeptDO secondTop = deptMapper.selectById(secondTopId);
assertEquals("ZT002", secondTop.getCode()); 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 @Test
public void testCreateDept_topLevelAutoCode_ignoreCustomInput() { public void testCreateDept_topLevelAutoCode_ignoreCustomInput() {
String customCode = "ROOT-001"; String customCode = "ROOT-001";
@@ -143,7 +321,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
topLevelReq.setName("集团"); topLevelReq.setName("集团");
topLevelReq.setSort(1); topLevelReq.setSort(1);
topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
topLevelReq.setDeptSource(1); topLevelReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
topLevelReq.setCode(customCode); topLevelReq.setCode(customCode);
Long deptId = deptService.createDept(topLevelReq); Long deptId = deptService.createDept(topLevelReq);
@@ -166,7 +344,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setParentId(DeptDO.PARENT_ID_ROOT); o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setId(dbDeptDO.getId()); o.setId(dbDeptDO.getId());
o.setStatus(randomCommonStatus()); o.setStatus(randomCommonStatus());
}).setDeptSource(1); }).setDeptSource(DeptSourceEnum.SYNC.getSource());
reqVO.setCode(dbDeptDO.getCode()); reqVO.setCode(dbDeptDO.getCode());
// 调用 // 调用
@@ -195,7 +373,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq.setParentId(parentBId); updateReq.setParentId(parentBId);
updateReq.setSort(1); updateReq.setSort(1);
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setDeptSource(1); updateReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
deptService.updateDept(updateReq); deptService.updateDept(updateReq);
DeptDO updatedChild = deptMapper.selectById(childId); DeptDO updatedChild = deptMapper.selectById(childId);
@@ -223,7 +401,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq1.setName("多系统部门"); updateReq1.setName("多系统部门");
updateReq1.setSort(1); updateReq1.setSort(1);
updateReq1.setStatus(CommonStatusEnum.ENABLE.getStatus()); updateReq1.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq1.setDeptSource(1); updateReq1.setDeptSource(DeptSourceEnum.SYNC.getSource());
updateReq1.setExternalSystemCode("ERP"); updateReq1.setExternalSystemCode("ERP");
updateReq1.setExternalDeptCode("ERP-100"); updateReq1.setExternalDeptCode("ERP-100");
deptService.updateDept(updateReq1); deptService.updateDept(updateReq1);
@@ -235,7 +413,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq2.setName("多系统部门"); updateReq2.setName("多系统部门");
updateReq2.setSort(1); updateReq2.setSort(1);
updateReq2.setStatus(CommonStatusEnum.ENABLE.getStatus()); updateReq2.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq2.setDeptSource(1); updateReq2.setDeptSource(DeptSourceEnum.SYNC.getSource());
updateReq2.setExternalSystemCode("OA"); updateReq2.setExternalSystemCode("OA");
updateReq2.setExternalDeptCode("OA-100"); updateReq2.setExternalDeptCode("OA-100");
deptService.updateDept(updateReq2); deptService.updateDept(updateReq2);
@@ -257,7 +435,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
createA.setName("iWork-A"); createA.setName("iWork-A");
createA.setSort(1); createA.setSort(1);
createA.setStatus(CommonStatusEnum.ENABLE.getStatus()); createA.setStatus(CommonStatusEnum.ENABLE.getStatus());
createA.setDeptSource(1); createA.setDeptSource(DeptSourceEnum.SYNC.getSource());
createA.setExternalSystemCode("IWORK"); createA.setExternalSystemCode("IWORK");
createA.setExternalDeptCode("IW-001"); createA.setExternalDeptCode("IW-001");
Long deptAId = deptService.createDept(createA); Long deptAId = deptService.createDept(createA);
@@ -272,7 +450,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
createB.setName("iWork-B"); createB.setName("iWork-B");
createB.setSort(2); createB.setSort(2);
createB.setStatus(CommonStatusEnum.ENABLE.getStatus()); createB.setStatus(CommonStatusEnum.ENABLE.getStatus());
createB.setDeptSource(1); createB.setDeptSource(DeptSourceEnum.SYNC.getSource());
createB.setExternalSystemCode("IWORK"); createB.setExternalSystemCode("IWORK");
createB.setExternalDeptCode("IW-001"); createB.setExternalDeptCode("IW-001");
Long deptBId = deptService.createDept(createB); Long deptBId = deptService.createDept(createB);
@@ -300,7 +478,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
updateReq.setName("子-更新"); updateReq.setName("子-更新");
updateReq.setSort(1); updateReq.setSort(1);
updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); updateReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
updateReq.setDeptSource(1); updateReq.setDeptSource(DeptSourceEnum.SYNC.getSource());
updateReq.setExternalSystemCode("IWORK"); updateReq.setExternalSystemCode("IWORK");
updateReq.setExternalDeptCode("IW-CHILD"); updateReq.setExternalDeptCode("IW-CHILD");
@@ -474,7 +652,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testGetDept() { public void testGetDept() {
// mock 数据 // mock 数据
DeptDO deptDO = randomPojo(DeptDO.class).setDeptSource(1); DeptDO deptDO = randomPojo(DeptDO.class).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(deptDO); deptMapper.insert(deptDO);
// 准备参数 // 准备参数
Long id = deptDO.getId(); Long id = deptDO.getId();
@@ -488,9 +666,9 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testGetDeptList_ids() { public void testGetDeptList_ids() {
// mock 数据 // mock 数据
DeptDO deptDO01 = randomPojo(DeptDO.class).setDeptSource(1); DeptDO deptDO01 = randomPojo(DeptDO.class).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(deptDO01); deptMapper.insert(deptDO01);
DeptDO deptDO02 = randomPojo(DeptDO.class).setDeptSource(1); DeptDO deptDO02 = randomPojo(DeptDO.class).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(deptDO02); deptMapper.insert(deptDO02);
// 准备参数 // 准备参数
List<Long> ids = Arrays.asList(deptDO01.getId(), deptDO02.getId()); List<Long> ids = Arrays.asList(deptDO01.getId(), deptDO02.getId());
@@ -511,7 +689,7 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setParentId(DeptDO.PARENT_ID_ROOT); o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setSort(1); o.setSort(1);
}).setDeptSource(1); }).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept); deptMapper.insert(dept);
// 测试 name 不匹配 // 测试 name 不匹配
deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> { deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> {
@@ -542,14 +720,14 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setParentId(DeptDO.PARENT_ID_ROOT); o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setSort(1); o.setSort(1);
}).setDeptSource(1); }).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept1); deptMapper.insert(dept1);
DeptDO dept2 = randomPojo(DeptDO.class, o -> { DeptDO dept2 = randomPojo(DeptDO.class, o -> {
o.setName("集团二部"); o.setName("集团二部");
o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setParentId(DeptDO.PARENT_ID_ROOT); o.setParentId(DeptDO.PARENT_ID_ROOT);
o.setSort(2); o.setSort(2);
}).setDeptSource(1); }).setDeptSource(DeptSourceEnum.SYNC.getSource());
deptMapper.insert(dept2); deptMapper.insert(dept2);
DeptDO otherDept = randomPojo(DeptDO.class, o -> { DeptDO otherDept = randomPojo(DeptDO.class, o -> {
o.setName("其他部门"); o.setName("其他部门");
@@ -573,14 +751,14 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testGetChildDeptList() { public void testGetChildDeptList() {
// mock 数据1 级别子节点) // 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); 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); deptMapper.insert(dept2);
// mock 数据2 级子节点) // 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); 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); deptMapper.insert(dept2a);
// 准备参数 // 准备参数
Long id = dept1.getParentId(); Long id = dept1.getParentId();
@@ -596,14 +774,14 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
@Test @Test
public void testGetChildDeptListFromCache() { public void testGetChildDeptListFromCache() {
// mock 数据1 级别子节点) // 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); 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); deptMapper.insert(dept2);
// mock 数据2 级子节点) // 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); 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); deptMapper.insert(dept2a);
// 准备参数 // 准备参数
Long id = dept1.getParentId(); Long id = dept1.getParentId();
@@ -689,4 +867,48 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
assertEquals("ZT001002", updatedChild2.getCode()); 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; package com.zt.plat.module.system.service.permission;
import com.zt.plat.framework.common.exception.ServiceException; 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.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.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.RoleDO;
import com.zt.plat.module.system.dal.dataobject.permission.RoleMenuDO; 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.RoleMenuMapper;
import com.zt.plat.module.system.dal.mysql.permission.UserRoleMapper; 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.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.enums.permission.RoleTypeEnum;
import com.zt.plat.module.system.service.dept.DeptService; 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.user.AdminUserService;
@@ -408,4 +411,54 @@ public class PermissionServiceTest extends BaseDbUnitTest {
assertEquals(1, exclusionDOS.size()); assertEquals(1, exclusionDOS.size());
assertEquals(101L, exclusionDOS.get(0).getMenuId()); 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" ( CREATE TABLE IF NOT EXISTS "system_dept" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "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 '', "name" varchar(30) NOT NULL DEFAULT '',
"short_name" varchar(30) DEFAULT '', "short_name" varchar(30) DEFAULT '',
"parent_id" bigint NOT NULL DEFAULT '0', "parent_id" bigint NOT NULL DEFAULT '0',
@@ -51,9 +51,8 @@ CREATE TABLE IF NOT EXISTS "system_dept_external_code" (
PRIMARY KEY ("id") PRIMARY KEY ("id")
) COMMENT '部门外部组织编码映射'; ) 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 INDEX IF NOT EXISTS "idx_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", "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 TABLE IF NOT EXISTS "system_dict_data" ( CREATE TABLE IF NOT EXISTS "system_dict_data" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,