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:
Ray.Hao committed 2026-09-08 10:06:41 +08:00
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();
}
}