feat: 新增动态表单与工作流模块
- module/form: 动态表单定义、数据、版本快照的增删改查与渲染校验 - module/workflow: Flowable 审批流,含流程设计、发起、审批中心与演示流程 - module/config: 工作流开关 flowable.enabled,关闭时不注册 Bean 并排除引擎自动配置 - auth: 按登录方式拆为 password/sms/qrcode/wxma/session 五个子包 - framework/security: 新增 config、handler,承载安全配置与认证授权失败处理 - application-*.yml: flowable 配置按 dev/prod/youlai 环境拆分 - sql/extensions: 新增 dynamic-form.sql、workflow.sql 增量脚本 - README: 补充目录结构、可选扩展与系统预览截图
This commit is contained in:
1 parent
3c9e24d978
commit
baf2dbe509
186 files changed
+5647
-934
No files matched your search
@@ -0,0 +1,61 @@
|
||||
package com.youlai.boot;
|
||||
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
|
||||
|
||||
/**
|
||||
* 分层依赖约束
|
||||
*
|
||||
* <p>固化包之间的依赖方向,防止 common 反向依赖上层、业务模块之间形成环。</p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.8.0
|
||||
*/
|
||||
@AnalyzeClasses(packages = "com.youlai.boot")
|
||||
class ArchitectureTest {
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule commonShouldNotDependOnUpperLayers =
|
||||
noClasses().that().resideInAPackage("com.youlai.boot.common..")
|
||||
.should().dependOnClassesThat().resideInAnyPackage(
|
||||
"com.youlai.boot.config..",
|
||||
"com.youlai.boot.framework..",
|
||||
"com.youlai.boot.system..",
|
||||
"com.youlai.boot.module..",
|
||||
"com.youlai.boot.auth..",
|
||||
"com.youlai.boot.codegen..",
|
||||
"com.youlai.boot.support..")
|
||||
.because("common 是纯共享层,只提供基础类型,不感知装配层、框架层和业务模块");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule frameworkShouldNotDependOnBusinessModules =
|
||||
noClasses().that().resideInAPackage("com.youlai.boot.framework..")
|
||||
.should().dependOnClassesThat().resideInAnyPackage(
|
||||
"com.youlai.boot.system..",
|
||||
"com.youlai.boot.module..",
|
||||
"com.youlai.boot.auth..",
|
||||
"com.youlai.boot.codegen..")
|
||||
.because("framework 提供通用技术能力,不依赖具体业务模块");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule systemShouldNotDependOnCodegen =
|
||||
noClasses().that().resideInAPackage("com.youlai.boot.system..")
|
||||
.should().dependOnClassesThat().resideInAPackage("com.youlai.boot.codegen..")
|
||||
.because("依赖方向为 codegen -> system,system 不得反向依赖代码生成");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule controllersShouldNotDependOnMappers =
|
||||
noClasses().that().haveSimpleNameEndingWith("Controller")
|
||||
.should().dependOnClassesThat().haveSimpleNameEndingWith("Mapper")
|
||||
.because("Controller 只能通过 Service 访问数据层");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule businessPackagesShouldBeFreeOfCycles =
|
||||
slices().matching("com.youlai.boot.(*)..")
|
||||
.should().beFreeOfCycles()
|
||||
.because("顶层包之间应单向依赖,出现环说明模块边界被绕过");
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.youlai.boot.validation;
|
||||
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* 参数校验约束测试
|
||||
*
|
||||
* <p>验证关键 Form 的约束行为,并扫描全部 Controller 保证每个 @RequestBody 参数都触发 Bean Validation。</p>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.8.0
|
||||
*/
|
||||
class ValidationTest {
|
||||
|
||||
private static Validator validator;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() {
|
||||
validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordResetFormShouldRejectBlankAndShortPassword() {
|
||||
var blank = new com.youlai.boot.system.model.form.PasswordResetForm();
|
||||
assertThat(validator.validate(blank)).isNotEmpty();
|
||||
|
||||
var form = new com.youlai.boot.system.model.form.PasswordResetForm();
|
||||
form.setPassword("12345");
|
||||
assertThat(validator.validate(form)).isNotEmpty();
|
||||
|
||||
form.setPassword("123456");
|
||||
assertThat(validator.validate(form)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void passwordUpdateFormShouldRequireAllThreeFields() {
|
||||
var form = new com.youlai.boot.system.model.form.PasswordUpdateForm();
|
||||
assertThat(validator.validate(form)).hasSize(3);
|
||||
|
||||
form.setOldPassword("123456");
|
||||
form.setNewPassword("123456");
|
||||
form.setConfirmPassword("123456");
|
||||
assertThat(validator.validate(form)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void baseQueryShouldRejectOutOfRangePaging() {
|
||||
var query = new com.youlai.boot.system.model.query.UserQuery();
|
||||
assertThat(validator.validate(query)).isEmpty();
|
||||
|
||||
query.setPageNum(0);
|
||||
assertThat(validator.validate(query)).isNotEmpty();
|
||||
|
||||
query.setPageNum(1);
|
||||
query.setPageSize(201);
|
||||
assertThat(validator.validate(query)).isNotEmpty();
|
||||
|
||||
query.setPageSize(200);
|
||||
query.setOrder("DROP TABLE");
|
||||
assertThat(validator.validate(query)).isNotEmpty();
|
||||
|
||||
query.setOrder("DESC");
|
||||
assertThat(validator.validate(query)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void genConfigFormShouldRequireCoreFields() {
|
||||
var form = new com.youlai.boot.codegen.model.form.GenConfigForm();
|
||||
assertThat(validator.validate(form)).isNotEmpty();
|
||||
|
||||
form.setTableName("sys_user");
|
||||
form.setBusinessName("用户");
|
||||
form.setModuleName("system");
|
||||
form.setPackageName("com.youlai");
|
||||
form.setEntityName("User");
|
||||
assertThat(validator.validate(form)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyRequestBodyParameterShouldTriggerValidation() throws Exception {
|
||||
var scanner = new org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider(false);
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(RestController.class));
|
||||
|
||||
List<String> violations = new ArrayList<>();
|
||||
for (var definition : scanner.findCandidateComponents("com.youlai.boot")) {
|
||||
Class<?> controller = Class.forName(definition.getBeanClassName());
|
||||
for (Method method : controller.getDeclaredMethods()) {
|
||||
for (Parameter parameter : method.getParameters()) {
|
||||
// Bean Validation 只作用于 Bean 类型,Map/List 等动态数据由 Service 层业务校验
|
||||
boolean isBeanType = parameter.getType().getName().startsWith("com.youlai.boot");
|
||||
boolean missingValidation = isBeanType
|
||||
&& parameter.isAnnotationPresent(RequestBody.class)
|
||||
&& !parameter.isAnnotationPresent(Valid.class)
|
||||
&& !parameter.isAnnotationPresent(Validated.class);
|
||||
if (missingValidation) {
|
||||
violations.add(controller.getSimpleName() + "#" + method.getName()
|
||||
+ " 的参数 " + parameter.getName() + " 缺少 @Valid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertThat(violations).as(String.join(";", violations)).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user