From 538153a4f0b460aee2f6fdebf79bc9e8d652b26b Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Sun, 1 Nov 2026 19:02:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(workflow):=20=E4=BF=AE=E5=A4=8D=E9=A9=B3?= =?UTF-8?q?=E5=9B=9E=E6=97=A0=E6=B3=95=E9=80=80=E5=9B=9E=E5=8F=91=E8=B5=B7?= =?UTF-8?q?=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增发起申请节点 - 发起表单改挂该节点 - 首次发起跳过发起人 - 驳回目标包含发起人 - 审批接口新增表单数据入参 - 新增表单数据更新逻辑 - 模型模板加入发起人节点 - AI 与流程引擎默认值移入公共配置 --- .gitignore | 2 + README.en.md | 66 ++- README.md | 66 ++- boot-wf.err | 1 + docs/images/logo/logo.png | Bin 19649 -> 28940 bytes docs/images/qrcode/wechat-mp.jpg | Bin 42954 -> 54211 bytes docs/images/qrcode/wechat-official.jpg | Bin 27403 -> 27425 bytes pom.xml | 48 +- sql/extensions/workflow.sql | 2 +- .../boot/auth/oidc/config/OidcProperties.java | 9 +- .../password/config/CaptchaProperties.java | 54 +-- .../password/exception/CaptchaException.java | 2 +- .../sms/token/SmsAuthenticationToken.java | 2 +- .../boot/auth/wxma/config/WxMaProperties.java | 4 +- .../wxma/token/WxMaAuthenticationToken.java | 2 +- .../boot/codegen/enums/JavaTypeEnum.java | 3 +- .../codegen/property/CodegenProperties.java | 22 +- .../boot/common/annotation/ValidField.java | 4 +- .../common/exception/RateLimitException.java | 2 +- .../youlai/boot/common/result/PageResult.java | 4 +- .../com/youlai/boot/config/CacheConfig.java | 13 + .../youlai/boot/config/CaffeineConfig.java | 37 -- .../youlai/boot/config/RedisCacheConfig.java | 30 +- .../youlai/boot/config/SecurityConfig.java | 3 - .../com/youlai/boot/config/XxlJobConfig.java | 13 +- .../security/config/SecurityProperties.java | 9 +- .../security/port/PermissionPort.java | 4 +- .../security/service/PermissionService.java | 4 +- .../service/SecurityUserDetailsService.java | 2 +- .../web/filter/IpRateLimitFilter.java | 4 +- .../web/log}/OperationLogExecutorConfig.java | 2 +- .../framework/web/ratelimit/RateLimit.java | 4 +- .../web/ratelimit/RateLimitProperties.java | 12 +- .../module/form/service/FormDataService.java | 8 + .../service/impl/FormDataServiceImpl.java | 16 + .../config/ConditionalOnWorkflowEnabled.java | 2 +- .../workflow/model/form/CompleteTaskForm.java | 3 + .../runner/WorkflowDemoInitializer.java | 30 -- .../impl/WorkflowInstanceServiceImpl.java | 6 + .../impl/WorkflowModelServiceImpl.java | 25 +- .../service/impl/WorkflowTaskServiceImpl.java | 14 +- .../module/workflow/util/BpmnModelHelper.java | 12 +- .../file/property/StorageProperties.java | 13 +- .../service/impl/AliyunFileServiceImpl.java | 21 +- .../service/impl/LocalFileServiceImpl.java | 8 +- .../file/service/impl/S3FileServiceImpl.java | 26 +- .../boot/support/mail/MailProperties.java | 26 +- .../support/mail/service/EmailService.java | 8 +- .../service/impl/SmtpEmailServiceImpl.java | 11 +- .../realtime/config/RealtimeProperties.java | 2 + .../boot/support/sms/AliyunSmsProperties.java | 18 +- .../boot/support/sms/service/SmsService.java | 4 +- .../adapter/security/PermissionAdapter.java | 2 +- .../system/controller/FileController.java | 2 +- .../system/controller/HealthController.java | 4 +- .../system/controller/UserController.java | 18 +- .../boot/system/schedule/XxlJobSample.java | 2 +- .../boot/system/service/UserImportReader.java | 2 +- ...itional-spring-configuration-metadata.json | 135 ++---- src/main/resources/application-dev.yml | 281 ++++------- src/main/resources/application-prod.yml | 298 ++++-------- src/main/resources/application.yml | 64 ++- .../codegen/frontend/js/index.crud.js.vue.vm | 452 +++++------------- src/main/resources/excel/用户导入模板.xlsx | Bin 12655 -> 0 bytes src/main/resources/logback-spring.xml | 6 +- .../resources/workflow/expense.bpmn20.xml | 55 ++- src/main/resources/workflow/leave.bpmn20.xml | 47 +- .../resources/workflow/purchase.bpmn20.xml | 55 ++- src/main/resources/workflow/seal.bpmn20.xml | 33 +- .../boot/config/CacheConfigurationTest.java | 173 +++++++ .../config/ConfigurationDefaultsTest.java | 146 ++++++ .../OptionalIntegrationConfigurationTest.java | 127 +++++ .../config/WorkflowConfigurationTest.java | 60 +++ .../boot/support/ai/AiConfigurationTest.java | 120 +++++ 74 files changed, 1582 insertions(+), 1183 deletions(-) create mode 100644 boot-wf.err create mode 100644 src/main/java/com/youlai/boot/config/CacheConfig.java delete mode 100644 src/main/java/com/youlai/boot/config/CaffeineConfig.java rename src/main/java/com/youlai/boot/{config => framework/web/log}/OperationLogExecutorConfig.java (95%) delete mode 100644 src/main/java/com/youlai/boot/module/workflow/runner/WorkflowDemoInitializer.java delete mode 100644 src/main/resources/excel/用户导入模板.xlsx create mode 100644 src/test/java/com/youlai/boot/config/CacheConfigurationTest.java create mode 100644 src/test/java/com/youlai/boot/config/ConfigurationDefaultsTest.java create mode 100644 src/test/java/com/youlai/boot/config/OptionalIntegrationConfigurationTest.java create mode 100644 src/test/java/com/youlai/boot/module/workflow/config/WorkflowConfigurationTest.java create mode 100644 src/test/java/com/youlai/boot/support/ai/AiConfigurationTest.java diff --git a/.gitignore b/.gitignore index 4b294208..fd24cbde 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ logs .history application-youlai.yml +/boot-run*.log +/boot-run*.err diff --git a/README.en.md b/README.en.md index 18138d06..7696df5b 100644 --- a/README.en.md +++ b/README.en.md @@ -30,7 +30,7 @@ ## Core Features -- 🔐 **Security** — Spring Security + JWT/Redis dual-session token model, token renewal, multi-device mutual exclusion +- 🔐 **Security** — Spring Security + JWT/Redis Token formats, unified session management, token renewal, multi-device mutual exclusion - 🔑 **Unified authentication (optional, disabled by default)** — [OIDC login](../docs/youlai-boot/oidc/implementation.md), delegates sign-in to an external identity provider and binds the authenticated account to a local user; nothing is loaded unless you enable it @@ -100,14 +100,26 @@ > Every extension script is **optional**. Users, roles, menus, dictionaries, logs and the code generator all live in `sql/youlai-admin.sql`; importing the base schema alone is enough to run the system. > - > `oidc.sql` targets deployments that already run an identity provider: it only writes the "App Center" menu and one demo identity binding. Single sign-on is disabled by default (`security.oidc.enabled` plus the frontend `VITE_OIDC_ENABLED`); without an issuer, client secret and frontend switch no OIDC component is loaded, so the base features are unaffected. Skip it if you do not need single sign-on. + > `oidc.sql` targets deployments with an existing identity provider. It adds the "App Center" menu and one demo identity binding. The backend switch `security.oidc.enabled` defaults to false. Enabling it requires an issuer, client ID, client secret, and frontend callback URI. The frontend login entry is controlled separately by `VITE_OIDC_ENABLED`. Skip this extension if you do not need single sign-on. -3. Adjust config (optional, a read-only online data source is configured by default): +3. Check the development configuration and set the MySQL and Redis connections for your environment: ```bash src/main/resources/application-dev.yml ``` + Configuration is ordered by basic services, security and access, files and API documentation, then optional integrations. Start with the first half; email, SMS, mini programs, scheduled jobs, OIDC, AI, and workflow are grouped at the end. Sections separated by `---` belong to the same environment and load together. + + `application.yml` holds shared rules. `application-dev.yml` and `application-prod.yml` expose common settings and integration credentials. CAPTCHA appearance, session lifetimes, and connection pools use defaults; add properties only when you need to override them. The IDE provides descriptions and default values. Configure file upload storage and credentials in the `storage` section. + + Empty strings (`""`) in optional integrations mean not configured. Fill in your credentials when using the service, or leave the section alone. Email defaults to port 587 with SMTP authentication and STARTTLS; override these for other SMTP servers. The mini program provider defaults to `wechat-mini-main`. Set a new `wx.miniapp.provider` when replacing the AppID to avoid reusing existing identity bindings. + + For local use, fill in YAML directly. Deployments can override it with external configuration files or native Spring Boot environment variables, such as `WX_MINIAPP_APPID` and `SECURITY_OIDC_ENABLED`, without adding placeholders to YAML. [Variable naming rules](https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables): replace dots with underscores, remove hyphens, and uppercase the name. Do not commit real credentials. + + Configure upload limits under `spring.servlet.multipart`; both the per-file and total request limits default to 50MB. When upgrading, migrate a custom `storage.upload.max-file-size` value to both `max-file-size` and `max-request-size`. + + Production SQL logging uses SLF4J and Logback. Temporarily set the relevant Mapper package to `DEBUG` when investigating SQL; the default `INFO` level does not print SQL debug logs. + 4. Start the service: **Option 1: Run in IDEA (recommended)** @@ -205,30 +217,46 @@ youlai-boot/ ## Optional Extensions -**Workflow (disabled by default)** +These are off by default. -Two steps to enable: +**AI**: fill the key in the active profile and add `spring.ai.model.chat: openai`. -1. Import the demo data (demo processes & demo accounts): +```yaml +spring: + ai: + model: + chat: openai + openai: + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + api-key: "" + chat: + model: qwen3.7-flash + temperature: 0.3 + extra-body: + "[enable_thinking]": false +``` - ```bash - sql/extensions/workflow.sql - ``` +**Workflow**: run the extension script to import menus, demo roles and forms (no `ACT_*` tables), then enable the switch in the active profile. - The ACT_* engine tables do not need to be imported — Flowable creates them automatically on startup (existing tables are left untouched). +```bash +sql/extensions/workflow.sql +``` -2. Enable it in `application-{profile}.yml`: +```yaml +flowable: + process: + enabled: true # single switch for the module and the engine + idm: + enabled: false + eventregistry: + enabled: false +``` - ```yaml - workflow: - enabled: true - ``` +Development creates engine tables on startup; production needs them created first. Demo processes come from the reset action on the process model page. - The Flowable engine switches (process / idm / eventregistry) follow `workflow.enabled` via placeholders in the same `application-{profile}.yml`, no extra maintenance needed. +**Single sign-on**: behind Nginx, uncomment `forward-headers-strategy: framework` in the profile's `server` block and make the proxy overwrite client `Forwarded` / `X-Forwarded-*` headers. -**Multi-tenant** - -For tenant isolation and tenant configuration, use the standalone repo [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant). +**Multi-tenant**: see [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant). ## License & Support diff --git a/README.md b/README.md index c45f66e2..4dacb353 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ## 核心特性 -- 🔐 **安全体系** — Spring Security + JWT/Redis Token 双会话模式、令牌续期、多端互斥 +- 🔐 **安全体系** — Spring Security + JWT/Redis Token 两种令牌格式、统一会话管理、令牌续期、多端互斥 - 🔑 **统一认证(可选,默认关闭)** — [OIDC 登录](../docs/youlai-boot/oidc/implementation.md),把登录交给外部认证中心,认证成功后关联本地用户并复用角色权限;不启用就不会加载相关组件 @@ -100,14 +100,26 @@ > 扩展脚本全部**可选**。用户、角色、菜单、字典、日志、代码生成等基础能力都在 `sql/youlai-admin.sql` 里,只导入基础库就能把系统跑起来。 > - > `oidc.sql` 面向"已经有认证中心"的场景,它只写入「应用中心」菜单和一条演示用的身份关联。单点登录本身默认关闭(`security.oidc.enabled` 与前端 `VITE_OIDC_ENABLED`),不填认证中心地址、客户端密钥和前端开关就不会加载 OIDC 组件,对基础功能零影响。暂时没有单点登录需求,跳过它即可。 + > `oidc.sql` 面向已有认证中心的场景,只写入「应用中心」菜单和一条演示身份关联。后端通过 `security.oidc.enabled` 控制 OIDC,默认关闭;开启时必须填写认证中心、客户端 ID、客户端密钥和前端回跳地址。前端登录入口通过 `VITE_OIDC_ENABLED` 单独控制。暂时没有单点登录需求,跳过即可。 -3. 修改配置(可选,默认已配置线上只读数据源): +3. 检查开发配置,按实际环境填写 MySQL、Redis 连接信息: ```bash src/main/resources/application-dev.yml ``` + 配置按「基础服务 → 安全与访问 → 文件与接口 → 可选扩展」排列。首次运行先看前半部分;邮件、短信、小程序、定时任务、OIDC、AI、工作流集中在后半部分,按需接入。`---` 分隔的各组都属于同一个环境,会一起加载。 + + `application.yml` 维护公共规则,`application-dev.yml` 和 `application-prod.yml` 只展开常用参数与接入信息。验证码外观、登录期限、连接池等沿用默认值,需要调整时再添加对应属性,IDE 可提示配置说明与默认值。文件上传的存储方式与凭据在 `storage` 段配置。 + + 可选服务中的 `""` 表示尚未接入,使用时填写自己的凭据;没有相关需求可以跳过。邮件默认使用端口 587、SMTP 认证和 STARTTLS,其他邮件服务按需覆盖。微信认证源默认 `wechat-mini-main`,首次接入无需填写 `provider`;更换 AppID 时需改用新的 `wx.miniapp.provider`,避免复用已有身份关联。 + + 本地接入可以直接填写 YAML,生产部署也可用外部配置文件或 Spring Boot 原生环境变量覆盖,例如 `WX_MINIAPP_APPID`、`SECURITY_OIDC_ENABLED`。无需在 YAML 中重复声明 `${变量名:默认值}`;[变量名转换规则](https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.typesafe-configuration-properties.relaxed-binding.environment-variables)为点改下划线、去掉连字符、转大写。实际密钥不要提交到版本库。 + + 上传大小在 `spring.servlet.multipart` 中配置,单文件和整个请求默认均为 50MB。升级时请把原 `storage.upload.max-file-size` 的自定义值分别迁移到 `max-file-size` 和 `max-request-size`。 + + 生产环境的 SQL 日志通过 SLF4J 接入 Logback。排查 SQL 时可临时将对应 Mapper 包的日志级别设为 `DEBUG`,默认 `INFO` 不输出 SQL 调试日志。 + 4. 启动服务: **方式一:IDEA 启动(推荐)** @@ -207,30 +219,46 @@ youlai-boot/ ## 可选扩展 -**工作流(默认关闭)** +以下功能默认关闭,需要时再开启。 -两步开启: +**AI 辅助**:在当前环境配置中填写密钥,并加上 `spring.ai.model.chat: openai`。 -1. 导入演示数据(演示流程与演示账号): +```yaml +spring: + ai: + model: + chat: openai + openai: + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + api-key: "" + chat: + model: qwen3.7-flash + temperature: 0.3 + extra-body: + "[enable_thinking]": false +``` - ```bash - sql/extensions/workflow.sql - ``` +**工作流**:先执行扩展脚本导入菜单、演示角色与发起表单(不含 `ACT_*` 引擎表),再在环境配置中打开开关。 - ACT_* 引擎表无需导入,启动时由 Flowable 自动创建(已存在则不重复处理)。 +```bash +sql/extensions/workflow.sql +``` -2. 在 `application-{profile}.yml` 中开启: +```yaml +flowable: + process: + enabled: true # 唯一开关,同时控制业务模块和引擎 + idm: + enabled: false + eventregistry: + enabled: false +``` - ```yaml - workflow: - enabled: true - ``` +开发环境启动时自动建表,生产需先建表;演示流程在「流程模型」页点「重置工作流数据」后生成。 - Flowable 引擎开关(process / idm / eventregistry)在 `application.yml` 中以占位符跟随 `workflow.enabled`,无需单独维护;只改上面一处即可。 +**统一认证**:经 Nginx 接入时,取消环境配置 `server` 块中 `forward-headers-strategy: framework` 的注释,并让代理覆盖客户端的 `Forwarded` / `X-Forwarded-*`。 -**多租户** - -需要租户隔离与租户配置,使用独立仓库 [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant)。 +**多租户**:见 [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant)。 ## 技术合作 diff --git a/boot-wf.err b/boot-wf.err new file mode 100644 index 00000000..ec6e7216 --- /dev/null +++ b/boot-wf.err @@ -0,0 +1 @@ +16:12:01,542 |-WARN in ch.qos.logback.core.model.processor.AppenderModelHandler - Appender named [FILE] not referenced. Skipping further processing. diff --git a/docs/images/logo/logo.png b/docs/images/logo/logo.png index ebfa2af601352e49bd6d93797b43f5b4f4d3bf5d..3b147dffa12f91ea6692bae23320a42b449c131d 100644 GIT binary patch literal 28940 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 literal 42954 zcmbrl2{@E(7zR3$WNYk-n2M-um92!SB*`yH*_kBEm^6P$CdSO4FqW7~nLmD}5)xyw z46=MX?(J|=Q_-hH*6LB|^Zl$GXkTSEf@8#crP(b0tsVPsZqz<{ z_i-&t$=K!$pmX8skg~3cEq|x*w`u>QW&iICOa1?3+5c+T|7I5lrYbK7ZJxXa3<;CT zc0I|4{U5KwcbR8x_E6$7xBO)VS~9L4pXF3f9Y3Z0&v|`Vg0|0L1?WQjg8+QE;2!G2 z-8IoU5365;DJinaIW8)|8jRegQ~kF| zL%|x1-LZ4z(df^~%a>-?C7XAr@-MX4{$nBXztWF!tgdw=e^Qab6{VR3TiGbG-fW>& z=)g1Ug6rc^LL!TtV`Mq(hIJm};m;A+oxD1qtxPz=Qt2l}}kzheMe~dLcPIR9wIlEh_!=?W4Z&E8u~l=ut`%SPlzf%8!!_N6o-2srl(mVFW1nQ%?#N{;g9RY z>$~JZth9m26OgB@MJFMrgPe=K4M#l%80Cod!DzwfE>uQ`G0^*%AG-f@PYdOii*XD= zguN%e(QD4pV*eVNsU-kAu*ze_j5rlQN8pb62pbh*#ylY^;|ZG;vIxm?U=a6kN6az zG%)|nkk_Z-(p{rir`-b26N~G=WnfV7=BC`9vNz;!Uu8-?!sMPc7=n17{C;J|n%vOt zdO}F=#!hni{#k84=;y(fWgl8TE~PcFh?PYNtsRqbv;hN*{myS;ffm@srw_v)OmFUV zLduk-YcK@_3fxIth!7w-3>WP%{KbwD37WUGZjJya1yk5TL=pM6)wj6EV+s8POXFZi z0_A)`oz1SgAJ#Qb8((C^m^26a4Vbb>$zzdy{)KLUBfG zu(K%%*Q&V6AUzPEjTT;SSq-~wmus~ex}9#ZKw4qdg?_JH66Jv{X3#+uNdtM=m<^TTg++F3C3_T|Y zK3NTPa2>(#1>WH=aqvUbSU%WBl-K;uXQ&0Ym#uuHy``5n&dVr9xGK0Obq?c^(?lrU}VD zEli- zo+}C(kag57eB`rOq@q>BSs~%>41HrL?vSrzbmr{Xqd~@^>GbUUmsou-@s|mVNsdXP zy(Xhg4?3$Kj@8Jm=AgC^rcp1V7k)rP8cL}N#7Vk{GGy0|;)}}YF(rE#%|LIHH3{$e z7)N2(d7I`}$Fy|s{a}FR2NzXgGNYQB8Ey41n;T!I&*YQq(T|57js*tZ`D=c&?$NV* zH_5%eSG`$2^=#^aZG(z$5VeU5(SU(SL0ZygAl^^tmu+naCc2<+_Noqh&#sD%$0Ug= ztH4`lsD9&&c4AQTiO*wp!QwZ!W<#8=R9_$DOtQB6jSu!x&-Cv(qFsODMBe6U|LvaY zcPp=NKdT$juyxn^{LU?vyB@*6{{Si$$>K-|CC(9f|Iy{ToW+A$saqYQ72x%ePPM)@GJIf?3i%i_#``9s9tjzdLEIuUeO2{p#yZnw>rjbhvdWdk>7M;Qc8%8y8x9fW_H~u^70(p4q6(TEziGh>WV2tr} zDCrr4UK_)2sRs?jM{&<(BkmwtSU%!a3((Ug=PKvR&I9j?1vv)htpiFQ0&q36=0)Cc z$q|6?tOLer~+JH5oHYX(aVTb5my~= z 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%0DQAYidx$tQ&J;Q%c7H#5;ipss`Ur5@!E?pI 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 diff --git a/docs/images/qrcode/wechat-official.jpg b/docs/images/qrcode/wechat-official.jpg index 576f210afa4a06336213fc0f0256feca7cb64041..507409ff7e8ccecb4415130a99807b9226441b31 100644 GIT binary patch delta 13289 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 F{{aCj+b#eA delta 13513 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 diff --git a/pom.xml b/pom.xml index 54182db8..99378d06 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.youlai youlai-boot - 4.14.0 + 4.14.1 基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。 @@ -68,7 +68,7 @@ 4.8.1.B - 2.0.0 + 2.0.2 @@ -129,7 +129,7 @@ test - + org.springframework.boot spring-boot-starter-webmvc-test @@ -143,7 +143,7 @@ test - + com.h2database h2 @@ -176,7 +176,7 @@ spring-boot-starter-cache - + org.springframework.boot spring-boot-starter-aspectj @@ -187,7 +187,7 @@ spring-boot-starter-validation - + org.springframework.ai spring-ai-starter-model-openai @@ -254,7 +254,7 @@ ${xxl-job.version} - + cn.idev.excel fastexcel @@ -341,6 +341,40 @@ ${project.artifactId} + + org.apache.maven.plugins + maven-compiler-plugin + + + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${project.parent.version} + + + + lombok.launch.AnnotationProcessorHider$AnnotationProcessor + org.mapstruct.ap.MappingProcessor + org.springframework.boot.configurationprocessor.ConfigurationMetadataAnnotationProcessor + + + org.apache.maven.plugins maven-surefire-plugin diff --git a/sql/extensions/workflow.sql b/sql/extensions/workflow.sql index c3f12f83..7bc68d1e 100644 --- a/sql/extensions/workflow.sql +++ b/sql/extensions/workflow.sql @@ -65,7 +65,7 @@ INSERT IGNORE INTO `sys_role_menu` VALUES (10, 91), (10, 9104), (10, 9105); -- ---------------------------- -- 演示流程的发起表单(依赖动态表单扩展) --- form_key 与 BPMN 开始节点 flowable:formKey 一一对应 +-- form_key 与 BPMN 发起人节点 flowable:formKey 一一对应 -- ID 段 1004-1007 顺延 dynamic-form.sql 预置的 1001-1003 -- ---------------------------- INSERT IGNORE INTO `form_definition` (`id`, `form_key`, `form_name`, `description`, `form_json`, `options_json`, `status`, `is_public`, `category`, `menu_id`, `version`, `create_time`, `update_time`, `create_by`, `update_by`, `is_deleted`) VALUES diff --git a/src/main/java/com/youlai/boot/auth/oidc/config/OidcProperties.java b/src/main/java/com/youlai/boot/auth/oidc/config/OidcProperties.java index 6487fa1c..3533faf2 100644 --- a/src/main/java/com/youlai/boot/auth/oidc/config/OidcProperties.java +++ b/src/main/java/com/youlai/boot/auth/oidc/config/OidcProperties.java @@ -20,24 +20,27 @@ import org.springframework.validation.annotation.Validated; @ConfigurationProperties(prefix = "security.oidc") public class OidcProperties { + /** 是否启用 OIDC 统一认证;启用后必须配置认证中心、客户端凭据和回跳地址 */ private boolean enabled = false; - // 身份表中的稳定标识;更换认证中心时使用新值或迁移关联 + /** 身份关联使用的稳定标识;更换认证中心时使用新值或迁移已有关联 */ @NotBlank @Pattern(regexp = "[a-z][a-z0-9-]{0,63}") private String provider = "oidc-main"; - // 必须与认证中心公布的 issuer 完全一致 + /** 认证中心地址,必须与其公布的 issuer 完全一致 */ @NotBlank private String issuer; + /** 认证中心注册的客户端 ID,启用时必填 */ @NotBlank private String clientId; + /** 客户端密钥,启用时必填;当前使用 client_secret_basic 认证 */ @NotBlank private String clientSecret; - // 认证成功后的前端回调地址 + /** 认证成功后的前端回跳地址,启用时必填 */ @NotBlank private String frontendCallbackUri; } diff --git a/src/main/java/com/youlai/boot/auth/password/config/CaptchaProperties.java b/src/main/java/com/youlai/boot/auth/password/config/CaptchaProperties.java index 7e7bda1c..e3ad9910 100644 --- a/src/main/java/com/youlai/boot/auth/password/config/CaptchaProperties.java +++ b/src/main/java/com/youlai/boot/auth/password/config/CaptchaProperties.java @@ -5,7 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** - * 验证码配置属性 + * 验证码配置属性;默认使用圆圈干扰的算术验证码,可按需覆盖 * * @author Ray.Hao * @since 2023/11/24 @@ -15,38 +15,38 @@ import org.springframework.stereotype.Component; @Data public class CaptchaProperties { - // 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 - private String type; + /** 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 */ + private String type = "circle"; - // 验证码图片宽度 - private int width; - // 验证码图片高度 - private int height; + /** 验证码图片宽度 */ + private int width = 120; + /** 验证码图片高度 */ + private int height = 40; - // 干扰线数量 - private int interfereCount; + /** 干扰线数量 */ + private int interfereCount = 2; - // 文本透明度 - private Float textAlpha; + /** 文本透明度 */ + private Float textAlpha = 0.8F; - // 验证码过期时间,单位:秒 - private Long expireSeconds; + /** 验证码过期时间,单位:秒 */ + private Long expireSeconds = 120L; - // 验证码字符配置 - private CodeProperties code; + /** 验证码字符配置 */ + private CodeProperties code = new CodeProperties(); - // 验证码字体 - private FontProperties font; + /** 验证码字体 */ + private FontProperties font = new FontProperties(); /** * 验证码字符配置 */ @Data public static class CodeProperties { - // 验证码字符类型 math-算术|random-随机字符串 - private String type; - // 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 - private int length; + /** 验证码字符类型 math-算术|random-随机字符串 */ + private String type = "math"; + /** 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 */ + private int length = 1; } /** @@ -54,12 +54,12 @@ public class CaptchaProperties { */ @Data public static class FontProperties { - // 字体名称 - private String name; - // 字体样式 0-普通|1-粗体|2-斜体 - private int weight; - // 字体大小 - private int size; + /** 字体名称 */ + private String name = "SansSerif"; + /** 字体样式 0-普通|1-粗体|2-斜体 */ + private int weight = 1; + /** 字体大小 */ + private int size = 24; } diff --git a/src/main/java/com/youlai/boot/auth/password/exception/CaptchaException.java b/src/main/java/com/youlai/boot/auth/password/exception/CaptchaException.java index 5e484b00..8ab98ea0 100644 --- a/src/main/java/com/youlai/boot/auth/password/exception/CaptchaException.java +++ b/src/main/java/com/youlai/boot/auth/password/exception/CaptchaException.java @@ -4,7 +4,7 @@ import com.youlai.boot.common.result.ResultCode; import lombok.Getter; /** - * 图形验证码校验失败时抛出的异常。 + * 图形验证码校验失败时抛出的异常 * * @author Ray.Hao * @since 3.1.0 diff --git a/src/main/java/com/youlai/boot/auth/sms/token/SmsAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/sms/token/SmsAuthenticationToken.java index 84e9d787..9ce48c4f 100644 --- a/src/main/java/com/youlai/boot/auth/sms/token/SmsAuthenticationToken.java +++ b/src/main/java/com/youlai/boot/auth/sms/token/SmsAuthenticationToken.java @@ -8,7 +8,7 @@ import java.io.Serial; import java.util.Collection; /** - * 短信验证码认证 Token。 + * 短信验证码认证 Token * * @author Ray.Hao * @since 2.20.0 diff --git a/src/main/java/com/youlai/boot/auth/wxma/config/WxMaProperties.java b/src/main/java/com/youlai/boot/auth/wxma/config/WxMaProperties.java index 41f70591..9d9c2987 100644 --- a/src/main/java/com/youlai/boot/auth/wxma/config/WxMaProperties.java +++ b/src/main/java/com/youlai/boot/auth/wxma/config/WxMaProperties.java @@ -26,9 +26,9 @@ public class WxMaProperties { @Pattern(regexp = "[a-z][a-z0-9-]{0,63}") private String provider = "wechat-mini-main"; - // 小程序 appid + /** 小程序 appid */ private String appid; - // 小程序 appsecret + /** 小程序 appsecret */ private String secret; } diff --git a/src/main/java/com/youlai/boot/auth/wxma/token/WxMaAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/wxma/token/WxMaAuthenticationToken.java index 24014e61..d6925118 100644 --- a/src/main/java/com/youlai/boot/auth/wxma/token/WxMaAuthenticationToken.java +++ b/src/main/java/com/youlai/boot/auth/wxma/token/WxMaAuthenticationToken.java @@ -8,7 +8,7 @@ import java.io.Serial; import java.util.Collection; /** - * 微信小程序认证 Token。 + * 微信小程序认证 Token * * @author Ray.Hao * @since 4.0.0 diff --git a/src/main/java/com/youlai/boot/codegen/enums/JavaTypeEnum.java b/src/main/java/com/youlai/boot/codegen/enums/JavaTypeEnum.java index 9c167fe2..de9660fb 100644 --- a/src/main/java/com/youlai/boot/codegen/enums/JavaTypeEnum.java +++ b/src/main/java/com/youlai/boot/codegen/enums/JavaTypeEnum.java @@ -23,7 +23,8 @@ public enum JavaTypeEnum { TINYINT("tinyint", "Integer", "number"), SMALLINT("smallint", "Integer", "number"), MEDIUMINT("mediumint", "Integer", "number"), - BIGINT("bigint", "Long", "number"), + // Long 与 BigInteger 由 Jackson 全局配置序列化为字符串,前端类型保持一致 + BIGINT("bigint", "Long", "string"), FLOAT("float", "Float", "number"), DOUBLE("double", "Double", "number"), DECIMAL("decimal", "BigDecimal", "number"), diff --git a/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java b/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java index 67ae9736..dea2979e 100644 --- a/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java +++ b/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java @@ -21,22 +21,22 @@ import java.util.Map; public class CodegenProperties { - // 默认配置 + /** 默认配置 */ private DefaultConfig defaultConfig ; - // 模板配置 + /** 模板配置 */ private Map templateConfigs = MapUtil.newHashMap(true); - // 后端应用名 + /** 后端应用名 */ private String backendAppName; - // 前端应用名 + /** 前端应用名 */ private String frontendAppName; - // 下载文件名 + /** 下载文件名 */ private String downloadFileName; - // 排除数据表 + /** 排除数据表 */ private List excludeTables; /** @@ -45,13 +45,13 @@ public class CodegenProperties { @Data public static class TemplateConfig { - // 模板路径,相对模板根 classpath:codegen (e.g. backend/controller.java.vm) + /** 模板路径,相对模板根 classpath:codegen (e.g. backend/controller.java.vm) */ private String templatePath; - // 子包名 (e.g. controller/service/mapper/model) + /** 子包名 (e.g. controller/service/mapper/model) */ private String subpackageName; - // 文件扩展名,如 .java + /** 文件扩展名,如 .java */ private String extension = FileNameUtil.EXT_JAVA; } @@ -62,10 +62,10 @@ public class CodegenProperties { @Data public static class DefaultConfig { - // 作者 (e.g. Ray) + /** 作者 (e.g. Ray) */ private String author; - // 默认模块名(e.g. system) + /** 默认模块名(e.g. system) */ private String moduleName; } diff --git a/src/main/java/com/youlai/boot/common/annotation/ValidField.java b/src/main/java/com/youlai/boot/common/annotation/ValidField.java index 559b06e1..d89b37dd 100644 --- a/src/main/java/com/youlai/boot/common/annotation/ValidField.java +++ b/src/main/java/com/youlai/boot/common/annotation/ValidField.java @@ -19,7 +19,7 @@ import java.lang.annotation.*; public @interface ValidField { /** - * 验证失败时的错误信息。 + * 验证失败时的错误信息 */ String message() default "非法字段"; @@ -28,7 +28,7 @@ public @interface ValidField { Class[] payload() default {}; /** - * 允许的合法值列表。 + * 允许的合法值列表 */ String[] allowedValues(); diff --git a/src/main/java/com/youlai/boot/common/exception/RateLimitException.java b/src/main/java/com/youlai/boot/common/exception/RateLimitException.java index 012bac2d..63da843d 100644 --- a/src/main/java/com/youlai/boot/common/exception/RateLimitException.java +++ b/src/main/java/com/youlai/boot/common/exception/RateLimitException.java @@ -4,7 +4,7 @@ import com.youlai.boot.common.result.ResultCode; import lombok.Getter; /** - * 接口限流异常:请求超出限流阈值时抛出,由全局异常处理器统一映射为 HTTP 429。 + * 接口限流异常:请求超出限流阈值时抛出,由全局异常处理器统一映射为 HTTP 429 * * @author Ray.Hao * @since 4.4.0 diff --git a/src/main/java/com/youlai/boot/common/result/PageResult.java b/src/main/java/com/youlai/boot/common/result/PageResult.java index b9df6bf0..214cf516 100644 --- a/src/main/java/com/youlai/boot/common/result/PageResult.java +++ b/src/main/java/com/youlai/boot/common/result/PageResult.java @@ -23,7 +23,7 @@ public class PageResult implements Serializable { private PageData data; /** - * 构建分页结果(MyBatis-Plus {@link IPage})。 + * 构建分页结果(MyBatis-Plus {@link IPage}) */ public static PageResult success(IPage page) { PageResult result = new PageResult<>(); @@ -43,7 +43,7 @@ public class PageResult implements Serializable { } /** - * 构建列表结果(无分页)。 + * 构建列表结果(无分页) */ public static PageResult success(List list) { PageResult result = new PageResult<>(); diff --git a/src/main/java/com/youlai/boot/config/CacheConfig.java b/src/main/java/com/youlai/boot/config/CacheConfig.java new file mode 100644 index 00000000..1740df95 --- /dev/null +++ b/src/main/java/com/youlai/boot/config/CacheConfig.java @@ -0,0 +1,13 @@ +package com.youlai.boot.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Configuration; + +/** + * 启用缓存注解,由 Spring Boot 根据 spring.cache.type 选择唯一的缓存管理器 + * none 使用空操作管理器,不缓存结果,也不影响登录会话等直接使用 Redis 的功能 + */ +@EnableCaching +@Configuration(proxyBeanMethods = false) +public class CacheConfig { +} diff --git a/src/main/java/com/youlai/boot/config/CaffeineConfig.java b/src/main/java/com/youlai/boot/config/CaffeineConfig.java deleted file mode 100644 index bba14491..00000000 --- a/src/main/java/com/youlai/boot/config/CaffeineConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.youlai.boot.config; - -import com.github.benmanes.caffeine.cache.Caffeine; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.CacheManager; -import org.springframework.cache.caffeine.CaffeineCacheManager; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * caffeine缓存配置 - * - * @author Theo - * @since 2025-01-22 17:40:23 - */ -@Slf4j -@Configuration -public class CaffeineConfig { - - @Value("${spring.cache.caffeine.spec}") - private String caffeineSpec; - - /** - * 缓存管理器 - * - * @return CacheManager 缓存管理器 - */ - @Bean - public CacheManager cacheManager() { - CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); - Caffeine caffeineBuilder = Caffeine.from(caffeineSpec); - caffeineCacheManager.setCaffeine(caffeineBuilder); - return caffeineCacheManager; - } -} - diff --git a/src/main/java/com/youlai/boot/config/RedisCacheConfig.java b/src/main/java/com/youlai/boot/config/RedisCacheConfig.java index bd8883d9..b72dc828 100644 --- a/src/main/java/com/youlai/boot/config/RedisCacheConfig.java +++ b/src/main/java/com/youlai/boot/config/RedisCacheConfig.java @@ -3,42 +3,23 @@ package com.youlai.boot.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.cache.autoconfigure.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.cache.RedisCacheWriter; -import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; /** - * Redis 缓存配置 + * Redis 缓存的序列化与过期策略,缓存管理器由 Spring Boot 自动装配 * * @author Ray.Hao * @since 2023/12/4 */ -@EnableCaching @EnableConfigurationProperties(CacheProperties.class) -@Configuration -@ConditionalOnProperty(name = "spring.cache.enabled") // 未配置视为关闭 +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(prefix = "spring.cache", name = "type", havingValue = "redis") public class RedisCacheConfig { - /** - * 自定义 RedisCacheManager - * - * @param redisConnectionFactory {@link RedisConnectionFactory} - * @param cacheProperties {@link CacheProperties} - * @return {@link RedisCacheManager} - */ - @Bean - public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties){ - return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) - .cacheDefaults(redisCacheConfiguration(cacheProperties)) - .build(); - } - /** * 自定义 RedisCacheConfiguration * @@ -61,11 +42,12 @@ public class RedisCacheConfig { if (!redisProperties.isCacheNullValues()) { config = config.disableCachingNullValues(); } + // 保留现有 cacheName: 前缀格式,并支持 spring.cache.redis.key-prefix + String keyPrefix = redisProperties.getKeyPrefix(); + config = config.computePrefixWith(name -> (keyPrefix != null ? keyPrefix : "") + name + ":"); if (!redisProperties.isUseKeyPrefix()) { config = config.disableKeyPrefix(); } - // 覆盖默认key双冒号 CacheKeyPrefix#prefixed - config = config.computePrefixWith(name -> name + ":"); return config; } diff --git a/src/main/java/com/youlai/boot/config/SecurityConfig.java b/src/main/java/com/youlai/boot/config/SecurityConfig.java index 038e50dd..d7df96fb 100644 --- a/src/main/java/com/youlai/boot/config/SecurityConfig.java +++ b/src/main/java/com/youlai/boot/config/SecurityConfig.java @@ -36,7 +36,6 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import jakarta.servlet.DispatcherType; -import org.springframework.http.HttpMethod; /** * Spring Security 配置类 @@ -63,8 +62,6 @@ public class SecurityConfig { .authorizeHttpRequests(requestMatcherRegistry -> { // 异步响应的后续分发复用首次请求认证结果 requestMatcherRegistry.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll(); - // 握手使用一次性票据;其余实时接口仍需 Bearer 认证 - requestMatcherRegistry.requestMatchers(HttpMethod.GET, "/api/v1/realtime/ws").permitAll(); String[] permitAllUrls = securityProperties.getPermitAllUrls(); if (ArrayUtil.isNotEmpty(permitAllUrls)) { requestMatcherRegistry.requestMatchers(permitAllUrls).permitAll(); diff --git a/src/main/java/com/youlai/boot/config/XxlJobConfig.java b/src/main/java/com/youlai/boot/config/XxlJobConfig.java index 909b677d..9af03377 100644 --- a/src/main/java/com/youlai/boot/config/XxlJobConfig.java +++ b/src/main/java/com/youlai/boot/config/XxlJobConfig.java @@ -8,11 +8,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** + * XXL-JOB 执行器配置;地址和 IP 默认自动获取,端口和日志策略可按需覆盖 * * @author xuxueli 2017-04-28 */ @Configuration -@ConditionalOnProperty(name = "xxl.job.enabled") // xxl.job.enabled = true 才会自动装配 +@ConditionalOnProperty(name = "xxl.job.enabled", havingValue = "true") @Slf4j public class XxlJobConfig { @@ -25,19 +26,19 @@ public class XxlJobConfig { @Value("${xxl.job.executor.appname}") private String appname; - @Value("${xxl.job.executor.address}") + @Value("${xxl.job.executor.address:}") private String address; - @Value("${xxl.job.executor.ip}") + @Value("${xxl.job.executor.ip:}") private String ip; - @Value("${xxl.job.executor.port}") + @Value("${xxl.job.executor.port:9999}") private int port; - @Value("${xxl.job.executor.logpath}") + @Value("${xxl.job.executor.logpath:/data/applogs/xxl-job/jobhandler}") private String logPath; - @Value("${xxl.job.executor.logretentiondays}") + @Value("${xxl.job.executor.logretentiondays:30}") private int logRetentionDays; diff --git a/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java b/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java index d00aa708..fa827ecb 100644 --- a/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java +++ b/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java @@ -25,11 +25,12 @@ import java.time.Duration; @ConfigurationProperties(prefix = "security") public class SecurityProperties { + /** 访问令牌格式与登录会话配置 */ @Valid @NotNull private TokenConfig token = new TokenConfig(); - // 匿名路径仍经过安全过滤器链 + /** 免登录路径,仍经过安全过滤器链 */ @NotEmpty private String[] permitAllUrls; @@ -39,18 +40,23 @@ public class SecurityProperties { @Data public static class TokenConfig { + /** 访问令牌格式:jwt 或 redis-token,两种格式共用 Redis 登录会话 */ @NotNull @Pattern(regexp = "jwt|redis-token", message = "令牌类型只能是 jwt 或 redis-token") private String type = "jwt"; + /** 访问令牌有效期,须为正整数秒且不超过会话最长有效期,如 2h */ @NotNull private Duration accessTokenTimeToLive = Duration.ofHours(2); + /** 登录会话最长有效期,刷新令牌不延长该期限,如 7d */ @NotNull private Duration refreshTokenTimeToLive = Duration.ofDays(7); + /** 是否允许多设备同时登录,false 时新登录会撤销已有会话 */ private boolean allowMultiLogin = true; + /** JWT 签名配置,仅 jwt 模式需要 */ private JwtConfig jwt; /** @@ -79,6 +85,7 @@ public class SecurityProperties { */ @Data public static class JwtConfig { + /** JWT 签名密钥,jwt 模式必填,至少 32 字节 UTF-8 */ private String secretKey; } } diff --git a/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java b/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java index 79fdfc80..137cda3b 100644 --- a/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java +++ b/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java @@ -3,14 +3,14 @@ package com.youlai.boot.framework.security.port; import java.util.Set; /** - * 权限查询端口。 + * 权限查询端口 * * @see com.youlai.boot.system.adapter.security.PermissionAdapter */ public interface PermissionPort { /** - * 根据角色编码集合查询权限标识集合。 + * 根据角色编码集合查询权限标识集合 * * @param roleCodes 角色编码集合 * @return 权限标识集合,如 "sys:user:create" diff --git a/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java b/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java index 074fd1ea..fb81a395 100644 --- a/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java +++ b/src/main/java/com/youlai/boot/framework/security/service/PermissionService.java @@ -12,7 +12,7 @@ import org.springframework.util.PatternMatchUtils; import java.util.Set; /** - * Spring Security 权限校验组件。 + * Spring Security 权限校验组件 * * @author Ray.Hao * @since 0.0.1 @@ -25,7 +25,7 @@ public class PermissionService { private final PermissionPort permissionPort; /** - * 判断当前用户是否拥有操作权限,支持通配符匹配。 + * 判断当前用户是否拥有操作权限,支持通配符匹配 */ public boolean hasPerm(String requiredPerm) { if (StrUtil.isBlank(requiredPerm)) { diff --git a/src/main/java/com/youlai/boot/framework/security/service/SecurityUserDetailsService.java b/src/main/java/com/youlai/boot/framework/security/service/SecurityUserDetailsService.java index 1dc4ced6..9e8113ff 100644 --- a/src/main/java/com/youlai/boot/framework/security/service/SecurityUserDetailsService.java +++ b/src/main/java/com/youlai/boot/framework/security/service/SecurityUserDetailsService.java @@ -11,7 +11,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; /** - * 系统用户认证 DetailsService。 + * 系统用户认证 DetailsService * * @author Ray.Hao * @since 2021/10/19 diff --git a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java index 50d8e993..508089b1 100644 --- a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java +++ b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java @@ -70,8 +70,8 @@ public class IpRateLimitFilter extends OncePerRequestFilter { } /** - * 设置渐进式限流响应头(X-RateLimit-Limit / Remaining / Reset)。 - * 超限时额外返回 Retry-After。 + * 设置渐进式限流响应头(X-RateLimit-Limit / Remaining / Reset) + * 超限时额外返回 Retry-After */ private void setRateLimitHeaders(HttpServletResponse response, int limit, diff --git a/src/main/java/com/youlai/boot/config/OperationLogExecutorConfig.java b/src/main/java/com/youlai/boot/framework/web/log/OperationLogExecutorConfig.java similarity index 95% rename from src/main/java/com/youlai/boot/config/OperationLogExecutorConfig.java rename to src/main/java/com/youlai/boot/framework/web/log/OperationLogExecutorConfig.java index 50988f3c..6fca4d29 100644 --- a/src/main/java/com/youlai/boot/config/OperationLogExecutorConfig.java +++ b/src/main/java/com/youlai/boot/framework/web/log/OperationLogExecutorConfig.java @@ -1,4 +1,4 @@ -package com.youlai.boot.config; +package com.youlai.boot.framework.web.log; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimit.java b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimit.java index 5ed95f94..92ed8eec 100644 --- a/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimit.java +++ b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimit.java @@ -18,12 +18,12 @@ import java.lang.annotation.Target; public @interface RateLimit { /** - * 窗口内允许的最大请求数。 + * 窗口内允许的最大请求数 */ int limit() default 0; /** - * 滑动窗口大小(秒)。 + * 滑动窗口大小(秒) */ int window() default 0; } diff --git a/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitProperties.java b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitProperties.java index 06c2b35b..3b878235 100644 --- a/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitProperties.java +++ b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitProperties.java @@ -17,25 +17,25 @@ import java.time.Duration; @ConfigurationProperties(prefix = "rate-limit") public class RateLimitProperties { - // / + /** 接口限流注解未指定 limit 时,窗口内允许的最大请求数 */ private int defaultLimit = 5; - // / + /** 接口限流注解未指定 window 时使用的滑动窗口时长 */ private Duration defaultWindow = Duration.ofSeconds(60); - // IP 全局限流配置 + /** IP 全局限流配置 */ private Ip ip = new Ip(); @Data public static class Ip { - // 是否启用 IP 全局限流。 + /** 是否启用 IP 全局限流 */ private boolean enabled = true; - // 窗口内最大请求数(默认 1000) + /** 窗口内最大请求数(默认 1000) */ private int limit = 1000; - // 滑动窗口大小,支持 60s / 1m / 500ms 等 Duration 写法(默认 60s) + /** 滑动窗口大小,支持 60s / 1m / 500ms 等 Duration 写法(默认 60s) */ private Duration window = Duration.ofSeconds(60); } diff --git a/src/main/java/com/youlai/boot/module/form/service/FormDataService.java b/src/main/java/com/youlai/boot/module/form/service/FormDataService.java index 283e9342..f57fc7c8 100644 --- a/src/main/java/com/youlai/boot/module/form/service/FormDataService.java +++ b/src/main/java/com/youlai/boot/module/form/service/FormDataService.java @@ -60,6 +60,14 @@ public interface FormDataService { */ FormDataVO getFormDataDetail(Long dataId); + /** + * 更新已提交的表单数据(发起人节点重新提交) + * + * @param dataId 数据ID + * @param data 表单数据(field -> value 映射) + */ + void updateFormData(Long dataId, Map data); + /** * 按表单标识删除全部提交数据(演示数据重置用) * diff --git a/src/main/java/com/youlai/boot/module/form/service/impl/FormDataServiceImpl.java b/src/main/java/com/youlai/boot/module/form/service/impl/FormDataServiceImpl.java index 34894fd4..62092019 100644 --- a/src/main/java/com/youlai/boot/module/form/service/impl/FormDataServiceImpl.java +++ b/src/main/java/com/youlai/boot/module/form/service/impl/FormDataServiceImpl.java @@ -206,6 +206,22 @@ public class FormDataServiceImpl extends ServiceImpl i this.removeByIds(ids); } + /** + * 更新已提交的表单数据(发起人节点重新提交) + * + * @param dataId 数据ID + * @param data 表单数据(field -> value 映射) + */ + @Override + public void updateFormData(Long dataId, Map data) { + FormData entity = this.getById(dataId); + if (entity == null) { + throw new BusinessException("表单数据不存在"); + } + entity.setDataJson(JSONUtil.toJsonStr(data)); + this.updateById(entity); + } + /** * 按表单标识删除全部提交数据(演示数据重置用) * diff --git a/src/main/java/com/youlai/boot/module/workflow/config/ConditionalOnWorkflowEnabled.java b/src/main/java/com/youlai/boot/module/workflow/config/ConditionalOnWorkflowEnabled.java index a2fd4e70..7c6e1290 100644 --- a/src/main/java/com/youlai/boot/module/workflow/config/ConditionalOnWorkflowEnabled.java +++ b/src/main/java/com/youlai/boot/module/workflow/config/ConditionalOnWorkflowEnabled.java @@ -17,6 +17,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProp @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented -@ConditionalOnBooleanProperty(prefix = "workflow", name = "enabled") +@ConditionalOnBooleanProperty(prefix = "flowable.process", name = "enabled") public @interface ConditionalOnWorkflowEnabled { } diff --git a/src/main/java/com/youlai/boot/module/workflow/model/form/CompleteTaskForm.java b/src/main/java/com/youlai/boot/module/workflow/model/form/CompleteTaskForm.java index 3d6111a9..82b64299 100644 --- a/src/main/java/com/youlai/boot/module/workflow/model/form/CompleteTaskForm.java +++ b/src/main/java/com/youlai/boot/module/workflow/model/form/CompleteTaskForm.java @@ -23,4 +23,7 @@ public class CompleteTaskForm { @Schema(description = "流程变量(驱动后续网关条件)") private Map variables; + @Schema(description = "发起表单数据(发起人节点重新提交时填写)") + private Map formData; + } diff --git a/src/main/java/com/youlai/boot/module/workflow/runner/WorkflowDemoInitializer.java b/src/main/java/com/youlai/boot/module/workflow/runner/WorkflowDemoInitializer.java deleted file mode 100644 index b571591b..00000000 --- a/src/main/java/com/youlai/boot/module/workflow/runner/WorkflowDemoInitializer.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.youlai.boot.module.workflow.runner; - -import com.youlai.boot.module.workflow.service.WorkflowDemoService; -import com.youlai.boot.module.workflow.config.ConditionalOnWorkflowEnabled; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -/** - * 演示流程启动初始化器 - * - * @author Ray.Hao - * @since 4.9.0 - */ -@ConditionalOnWorkflowEnabled -@Component -@ConditionalOnProperty(name = "workflow.demo.enabled", havingValue = "true") -@RequiredArgsConstructor -public class WorkflowDemoInitializer implements ApplicationRunner { - - private final WorkflowDemoService workflowDemoService; - - @Override - public void run(ApplicationArguments args) { - workflowDemoService.initIfAbsent(); - } - -} diff --git a/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowInstanceServiceImpl.java b/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowInstanceServiceImpl.java index 13207be2..de1795d4 100644 --- a/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowInstanceServiceImpl.java +++ b/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowInstanceServiceImpl.java @@ -71,6 +71,10 @@ public class WorkflowInstanceServiceImpl implements WorkflowInstanceService { Assert.isTrue(!definition.isSuspended(), "流程定义已挂起,无法发起"); Map variables = new HashMap<>(); + // 发起人节点按发起人办理;首次发起用跳过表达式直达审批,驳回时才停在发起人 + variables.put("initiator", SecurityUtils.getUsername()); + variables.put("_FLOWABLE_SKIP_EXPRESSION_ENABLED", true); + variables.put("_SKIP_INITIATOR", true); String businessKey = null; String formKey = bpmnModelHelper.getStartFormKey(definition.getId()); if (StrUtil.isNotBlank(formKey)) { @@ -97,6 +101,8 @@ public class WorkflowInstanceServiceImpl implements WorkflowInstanceService { .name(instanceName) .variables(variables) .start(); + // 首轮已跳过发起人,关闭跳过标记,后续驳回到发起人才会生成待办 + runtimeService.setVariable(instance.getId(), "_SKIP_INITIATOR", false); return instance.getId(); } finally { Authentication.setAuthenticatedUserId(null); diff --git a/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowModelServiceImpl.java b/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowModelServiceImpl.java index c77e8b7c..4f97cfa8 100644 --- a/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowModelServiceImpl.java +++ b/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowModelServiceImpl.java @@ -71,14 +71,20 @@ public class WorkflowModelServiceImpl implements WorkflowModelService { flow1 - + flow1 + flowInitiator + + + flowInitiator flow2 flow2 - + + @@ -86,19 +92,26 @@ public class WorkflowModelServiceImpl implements WorkflowModelService { - + + + + - + - + - + + + + + diff --git a/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowTaskServiceImpl.java b/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowTaskServiceImpl.java index 9ff67d35..7111f7f5 100644 --- a/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowTaskServiceImpl.java +++ b/src/main/java/com/youlai/boot/module/workflow/service/impl/WorkflowTaskServiceImpl.java @@ -241,7 +241,19 @@ public class WorkflowTaskServiceImpl implements WorkflowTaskService { Task task = getCurrentUserTask(taskId); this.addComment(task, formData.getComment()); this.claimIfCandidate(task); - taskService.complete(taskId, formData.getVariables() == null ? Map.of() : formData.getVariables()); + Map variables = formData.getVariables() == null + ? new HashMap<>() : new HashMap<>(formData.getVariables()); + if (formData.getFormData() != null && !formData.getFormData().isEmpty()) { + // 发起人节点重新提交:更新发起表单数据,字段同时作为流程变量驱动网关 + ProcessInstance instance = runtimeService.createProcessInstanceQuery() + .processInstanceId(task.getProcessInstanceId()) + .singleResult(); + Assert.isTrue(instance != null && StrUtil.isNotBlank(instance.getBusinessKey()), + "当前任务无关联的发起表单数据"); + formDataService.updateFormData(Long.valueOf(instance.getBusinessKey()), formData.getFormData()); + variables.putAll(formData.getFormData()); + } + taskService.complete(taskId, variables); } /** diff --git a/src/main/java/com/youlai/boot/module/workflow/util/BpmnModelHelper.java b/src/main/java/com/youlai/boot/module/workflow/util/BpmnModelHelper.java index 71de57ba..5b7bf976 100644 --- a/src/main/java/com/youlai/boot/module/workflow/util/BpmnModelHelper.java +++ b/src/main/java/com/youlai/boot/module/workflow/util/BpmnModelHelper.java @@ -74,9 +74,19 @@ public class BpmnModelHelper { if (process == null) { return null; } - return process.findFlowElementsOfType(StartEvent.class).stream() + String startFormKey = process.findFlowElementsOfType(StartEvent.class).stream() .findFirst() .map(StartEvent::getFormKey) + .filter(StrUtil::isNotBlank) + .orElse(null); + if (startFormKey != null) { + return startFormKey; + } + // 发起表单挂在发起人节点时,回退取首个用户任务的表单 + return process.findFlowElementsOfType(UserTask.class).stream() + .map(UserTask::getFormKey) + .filter(StrUtil::isNotBlank) + .findFirst() .orElse(null); } diff --git a/src/main/java/com/youlai/boot/support/file/property/StorageProperties.java b/src/main/java/com/youlai/boot/support/file/property/StorageProperties.java index 0ecc9e77..6845259a 100644 --- a/src/main/java/com/youlai/boot/support/file/property/StorageProperties.java +++ b/src/main/java/com/youlai/boot/support/file/property/StorageProperties.java @@ -20,10 +20,10 @@ import java.util.stream.Collectors; @ConfigurationProperties(prefix = "storage") public class StorageProperties { - // 存储类型,决定激活哪个 FileService 实现类 + /** 文件存储类型:s3、aliyun 或 local */ private String type; - // 上传限制 + /** 上传限制 */ private Upload upload = new Upload(); /** @@ -32,15 +32,12 @@ public class StorageProperties { @Data public static class Upload { - // 单文件大小上限,如 50MB - private String maxFileSize; - - // 允许上传的文件扩展名白名单 + /** 允许上传的文件扩展名白名单,留空不限制 */ private List allowedExtensions; } /** - * 获取规范化后的扩展名白名单 + * 从 upload.allowed-extensions 计算规范化后的扩展名白名单,不是独立配置项 * * @return 小写、不含点的扩展名集合;空集合表示不限制 */ @@ -55,4 +52,4 @@ public class StorageProperties { .map(String::toLowerCase) .collect(Collectors.toUnmodifiableSet()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/support/file/service/impl/AliyunFileServiceImpl.java b/src/main/java/com/youlai/boot/support/file/service/impl/AliyunFileServiceImpl.java index ce084cd7..156ea3e0 100644 --- a/src/main/java/com/youlai/boot/support/file/service/impl/AliyunFileServiceImpl.java +++ b/src/main/java/com/youlai/boot/support/file/service/impl/AliyunFileServiceImpl.java @@ -13,9 +13,11 @@ import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.support.file.model.FileInfo; import com.youlai.boot.support.file.service.FileService; import jakarta.annotation.PostConstruct; +import lombok.AccessLevel; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -26,7 +28,7 @@ import java.io.InputStream; import java.time.LocalDateTime; /** - * 阿里云 OSS 文件存储服务。 + * 阿里云 OSS 文件存储服务 */ @Component @Slf4j @@ -36,15 +38,16 @@ import java.time.LocalDateTime; @Data public class AliyunFileServiceImpl implements FileService { - // OSS 地域节点,如 {@code oss-cn-hangzhou.aliyuncs.com},兼容带 {@code http(s)://} 前缀写法 + /** OSS 地域节点,如 oss-cn-hangzhou.aliyuncs.com,兼容带 http(s):// 前缀写法 */ private String endpoint; - // 访问密钥 ID + /** 访问密钥 ID */ private String accessKeyId; - // 访问密钥 Secret + /** 访问密钥 Secret */ private String accessKeySecret; - // 存储空间名称 + /** 存储空间名称 */ private String bucket; + @Setter(AccessLevel.NONE) private OSS aliyunOssClient; @PostConstruct @@ -53,7 +56,7 @@ public class AliyunFileServiceImpl implements FileService { } /** - * 上传文件到阿里云 OSS,按日期分目录,返回 {@code 协议://bucket.endpoint/path} 格式的 URL。 + * 上传文件到阿里云 OSS,按日期分目录,返回 {@code 协议://bucket.endpoint/path} 格式的 URL */ @Override @SneakyThrows @@ -79,7 +82,7 @@ public class AliyunFileServiceImpl implements FileService { } /** - * 从完整 URL 中提取文件名后调用 OSS 删除。 + * 从完整 URL 中提取文件名后调用 OSS 删除 * * @param filePath 文件完整 URL * @return 删除成功返回 true @@ -94,7 +97,7 @@ public class AliyunFileServiceImpl implements FileService { /** * 补全 endpoint 协议前缀:已带 {@code http://} 或 {@code https://} 的仅去除尾部 {@code /}, - * 纯域名写法默认补 {@code https://},避免 OSS 客户端对无协议 endpoint 默认走 http 明文传输。 + * 纯域名写法默认补 {@code https://},避免 OSS 客户端对无协议 endpoint 默认走 http 明文传输 */ private String endpointWithProtocol() { String trimmed = StrUtil.trim(endpoint); @@ -105,7 +108,7 @@ public class AliyunFileServiceImpl implements FileService { } /** - * 获取文件访问主机地址(含协议),如 {@code https://bucket.oss-cn-hangzhou.aliyuncs.com}。 + * 获取文件访问主机地址(含协议),如 {@code https://bucket.oss-cn-hangzhou.aliyuncs.com} */ private String getFileHost() { String endpointWithProtocol = endpointWithProtocol(); diff --git a/src/main/java/com/youlai/boot/support/file/service/impl/LocalFileServiceImpl.java b/src/main/java/com/youlai/boot/support/file/service/impl/LocalFileServiceImpl.java index 1d00b2fc..afb6bf50 100644 --- a/src/main/java/com/youlai/boot/support/file/service/impl/LocalFileServiceImpl.java +++ b/src/main/java/com/youlai/boot/support/file/service/impl/LocalFileServiceImpl.java @@ -20,7 +20,7 @@ import java.io.InputStream; import java.time.LocalDateTime; /** - * 本地文件存储服务。 + * 本地文件存储服务 */ @Data @Slf4j @@ -30,11 +30,11 @@ import java.time.LocalDateTime; @RequiredArgsConstructor public class LocalFileServiceImpl implements FileService { - // 本地存储根目录,来自 {@code storage.local.path} + /** 本地存储根目录,来自 storage.local.path */ private String path; /** - * 上传文件到本地磁盘,按日期分目录,返回相对于存储根目录的路径。 + * 上传文件到本地磁盘,按日期分目录,返回相对于存储根目录的路径 */ @Override public FileInfo uploadFile(MultipartFile file) { @@ -57,7 +57,7 @@ public class LocalFileServiceImpl implements FileService { } /** - * 删除本地文件。路径是目录或不存在时返回 false。 + * 删除本地文件。路径是目录或不存在时返回 false * * @param filePath 相对于存储根目录的文件路径 * @return 删除成功返回 true diff --git a/src/main/java/com/youlai/boot/support/file/service/impl/S3FileServiceImpl.java b/src/main/java/com/youlai/boot/support/file/service/impl/S3FileServiceImpl.java index ad1cdd70..479c15e7 100644 --- a/src/main/java/com/youlai/boot/support/file/service/impl/S3FileServiceImpl.java +++ b/src/main/java/com/youlai/boot/support/file/service/impl/S3FileServiceImpl.java @@ -10,9 +10,11 @@ import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.support.file.model.FileInfo; import com.youlai.boot.support.file.service.FileService; import jakarta.annotation.PostConstruct; +import lombok.AccessLevel; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -52,18 +54,20 @@ import java.time.LocalDateTime; @Slf4j public class S3FileServiceImpl implements FileService { - // S3 对象存储服务端点,如 {@code http://localhost:9000} + /** S3 对象存储服务端点,如 http://localhost:9000 */ private String endpoint; - // 访问密钥 + /** 访问密钥 */ private String accessKey; - // 秘密密钥 + /** 秘密密钥 */ private String secretKey; - // 存储桶名称 + /** 存储桶名称 */ private String bucket; - // 自定义域名(可选),配置后不再使用预签名 URL + /** 文件访问域名,留空时使用对象存储端点生成访问地址 */ private String domain; + @Setter(AccessLevel.NONE) private S3Client s3Client; + @Setter(AccessLevel.NONE) private S3Presigner presigner; @PostConstruct @@ -90,7 +94,7 @@ public class S3FileServiceImpl implements FileService { } /** - * 上传文件到 S3(RustFS),按日期分目录,自动创建存储桶,返回文件访问 URL。 + * 上传文件到 S3(RustFS),按日期分目录,自动创建存储桶,返回文件访问 URL */ @Override @SneakyThrows @@ -129,8 +133,8 @@ public class S3FileServiceImpl implements FileService { } /** - * 生成预签名 GET URL 并去掉 query 参数,路径直达对象。 - * 依赖桶已开匿名只读(详见 createBucketIfAbsent 的策略),签名参数在 URL 中无实际作用。 + * 生成预签名 GET URL 并去掉 query 参数,路径直达对象 + * 依赖桶已开匿名只读(详见 createBucketIfAbsent 的策略),签名参数在 URL 中无实际作用 */ private String presignedUrl(String dateFolder, String fileName) { GetObjectRequest getObjectRequest = GetObjectRequest.builder() @@ -146,7 +150,7 @@ public class S3FileServiceImpl implements FileService { } /** - * 删除文件。 + * 删除文件 * * @param filePath 文件完整 URL * @return 删除成功返回 true @@ -182,7 +186,7 @@ public class S3FileServiceImpl implements FileService { } /** - * 存储桶不存在时自动创建,并设置为公开只读策略。 + * 存储桶不存在时自动创建,并设置为公开只读策略 */ @SneakyThrows private void createBucketIfAbsent(String bucket) { @@ -199,4 +203,4 @@ public class S3FileServiceImpl implements FileService { } } } -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/support/mail/MailProperties.java b/src/main/java/com/youlai/boot/support/mail/MailProperties.java index 41944bce..95471027 100644 --- a/src/main/java/com/youlai/boot/support/mail/MailProperties.java +++ b/src/main/java/com/youlai/boot/support/mail/MailProperties.java @@ -5,7 +5,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** - * 邮件配置属性 + * 邮件配置属性;默认使用 587 端口、SMTP 认证和 STARTTLS * * @author Ray * @since 2024/8/17 @@ -15,22 +15,22 @@ import org.springframework.stereotype.Component; @Data public class MailProperties { - // SMTP 服务器地址 + /** SMTP 服务器地址 */ private String host; - // SMTP 端口 - private int port; + /** SMTP 端口 */ + private int port = 587; - // 发件人账号 + /** 发件人账号 */ private String username; - // 发件人密码 + /** 发件人密码 */ private String password; - // 发件人地址 + /** 发件人地址 */ private String from; - // SMTP 扩展属性 + /** SMTP 扩展属性 */ private Properties properties = new Properties(); @Data @@ -39,16 +39,16 @@ public class MailProperties { @Data public static class Smtp { - // 启用 SMTP 认证 - private boolean auth; + /** 启用 SMTP 认证 */ + private boolean auth = true; private StartTls starttls = new StartTls(); @Data public static class StartTls { - // 启用 STARTTLS - private boolean enable; + /** 启用 STARTTLS */ + private boolean enable = true; } } } -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/support/mail/service/EmailService.java b/src/main/java/com/youlai/boot/support/mail/service/EmailService.java index 401e7294..902eb96d 100644 --- a/src/main/java/com/youlai/boot/support/mail/service/EmailService.java +++ b/src/main/java/com/youlai/boot/support/mail/service/EmailService.java @@ -1,7 +1,7 @@ package com.youlai.boot.support.mail.service; /** - * 邮件发送服务接口。 + * 邮件发送服务接口 * * @author Ray.Hao * @since 3.1.0 @@ -9,7 +9,7 @@ package com.youlai.boot.support.mail.service; public interface EmailService { /** - * 发送纯文本邮件。 + * 发送纯文本邮件 * * @param to 收件人邮箱 * @param subject 邮件主题 @@ -18,7 +18,7 @@ public interface EmailService { void send(String to, String subject, String text); /** - * 发送HTML格式的邮件。 + * 发送HTML格式的邮件 * * @param to 收件人邮箱 * @param subject 邮件主题 @@ -27,7 +27,7 @@ public interface EmailService { void sendHtmlMail(String to, String subject, String html); /** - * 发送带附件的邮件。 + * 发送带附件的邮件 * * @param to 收件人邮箱 * @param subject 邮件主题 diff --git a/src/main/java/com/youlai/boot/support/mail/service/impl/SmtpEmailServiceImpl.java b/src/main/java/com/youlai/boot/support/mail/service/impl/SmtpEmailServiceImpl.java index df87d87f..3df45cca 100644 --- a/src/main/java/com/youlai/boot/support/mail/service/impl/SmtpEmailServiceImpl.java +++ b/src/main/java/com/youlai/boot/support/mail/service/impl/SmtpEmailServiceImpl.java @@ -19,7 +19,7 @@ import java.io.File; import java.nio.charset.StandardCharsets; /** - * SMTP 邮件服务。 + * SMTP 邮件服务 * * @author Ray.Hao * @since 3.1.0 @@ -31,11 +31,11 @@ import java.nio.charset.StandardCharsets; public class SmtpEmailServiceImpl implements EmailService { private final JavaMailSender mailSender; - // 发件人地址等邮件配置,来自 {@code mail.*} + // 发件人地址等邮件配置,绑定 spring.mail.* private final MailProperties mailProperties; /** - * 发送纯文本邮件,发件人取自 {@code mail.from} 配置。 + * 发送纯文本邮件,发件人取自 {@code spring.mail.from} 配置 */ @Override public void send(String to, String subject, String text) { @@ -57,7 +57,6 @@ public class SmtpEmailServiceImpl implements EmailService { * @param to 收件人地址 * @param subject 邮件主题 * @param html html格式的内容 - * @return 是否发送成功 */ @Override public void sendHtmlMail(String to, String subject, String html) { @@ -82,7 +81,7 @@ public class SmtpEmailServiceImpl implements EmailService { } /** - * 发送带附件的 HTML 邮件。 + * 发送带附件的 HTML 邮件 */ @Override public void sendWithAttachment(String to, String subject, String text, String filePath) { @@ -102,4 +101,4 @@ public class SmtpEmailServiceImpl implements EmailService { log.error("发送带附件邮件失败: {}", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/support/realtime/config/RealtimeProperties.java b/src/main/java/com/youlai/boot/support/realtime/config/RealtimeProperties.java index f8619bb0..8467c60c 100644 --- a/src/main/java/com/youlai/boot/support/realtime/config/RealtimeProperties.java +++ b/src/main/java/com/youlai/boot/support/realtime/config/RealtimeProperties.java @@ -15,6 +15,8 @@ import java.util.List; @Setter @ConfigurationProperties("realtime") public class RealtimeProperties { + /** 是否启用实时通信接口 */ private boolean enabled = true; + /** 额外允许的浏览器来源,如 https://admin.example.com */ private List allowedOrigins = List.of(); } diff --git a/src/main/java/com/youlai/boot/support/sms/AliyunSmsProperties.java b/src/main/java/com/youlai/boot/support/sms/AliyunSmsProperties.java index 36f82dd9..4ad195f9 100644 --- a/src/main/java/com/youlai/boot/support/sms/AliyunSmsProperties.java +++ b/src/main/java/com/youlai/boot/support/sms/AliyunSmsProperties.java @@ -17,22 +17,22 @@ import java.util.Map; @ConfigurationProperties(prefix = "sms.aliyun") public class AliyunSmsProperties { - // 访问密钥 ID + /** 访问密钥 ID */ private String accessKeyId; - // 访问密钥 Secret + /** 访问密钥 Secret */ private String accessKeySecret; - // API 域名,如 dysmsapi.aliyuncs.com - private String domain; + /** API 域名,默认使用阿里云短信公共端点 */ + private String domain = "dysmsapi.aliyuncs.com"; - // 区域,如 cn-shanghai - private String regionId; + /** 区域,默认 cn-shanghai */ + private String regionId = "cn-shanghai"; - // 短信签名 + /** 短信签名 */ private String signName; - // 短信模板集合,key 为模板用途(login/register),value 为模板CODE + /** 短信模板集合,key 为模板用途(login/register),value 为模板CODE */ private Map templates; -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/support/sms/service/SmsService.java b/src/main/java/com/youlai/boot/support/sms/service/SmsService.java index 1922d48c..756ae139 100644 --- a/src/main/java/com/youlai/boot/support/sms/service/SmsService.java +++ b/src/main/java/com/youlai/boot/support/sms/service/SmsService.java @@ -5,7 +5,7 @@ import com.youlai.boot.support.sms.SmsTypeEnum; import java.util.Map; /** - * 短信发送服务接口。 + * 短信发送服务接口 * * @author Ray.Hao * @since 2024/8/17 @@ -13,7 +13,7 @@ import java.util.Map; public interface SmsService { /** - * 发送短信。 + * 发送短信 * * @param mobile 手机号 * @param smsType 短信类型,对应 {@code sms.templates.*} 配置中的模板 diff --git a/src/main/java/com/youlai/boot/system/adapter/security/PermissionAdapter.java b/src/main/java/com/youlai/boot/system/adapter/security/PermissionAdapter.java index fbd1a177..e408329e 100644 --- a/src/main/java/com/youlai/boot/system/adapter/security/PermissionAdapter.java +++ b/src/main/java/com/youlai/boot/system/adapter/security/PermissionAdapter.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Component; import java.util.Set; /** - * 权限查询适配器。 + * 权限查询适配器 * * @author Ray.Hao * @since 3.1.0 diff --git a/src/main/java/com/youlai/boot/system/controller/FileController.java b/src/main/java/com/youlai/boot/system/controller/FileController.java index fdaff799..ac720753 100644 --- a/src/main/java/com/youlai/boot/system/controller/FileController.java +++ b/src/main/java/com/youlai/boot/system/controller/FileController.java @@ -63,7 +63,7 @@ public class FileController { } /** - * 文件扩展名白名单校验:{@code allowed-extensions} 为空时不限制。 + * 文件扩展名白名单校验:{@code allowed-extensions} 为空时不限制 */ private void validateFileExtension(MultipartFile file) { Set allowedExtensions = storageProperties.getAllowedExtensions(); diff --git a/src/main/java/com/youlai/boot/system/controller/HealthController.java b/src/main/java/com/youlai/boot/system/controller/HealthController.java index 09b0652d..68cb177a 100644 --- a/src/main/java/com/youlai/boot/system/controller/HealthController.java +++ b/src/main/java/com/youlai/boot/system/controller/HealthController.java @@ -9,8 +9,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** - * 系统健康检查接口,返回 ok。 - * 不标注 @RateLimit,仅受 IP 全局限流层约束,用作该层的验证端点。 + * 系统健康检查接口,返回 ok + * 不标注 @RateLimit,仅受 IP 全局限流层约束,用作该层的验证端点 */ @Tag(name = "00.系统健康检查") @RestController diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java index 1eb41a57..0808ab4c 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -1,7 +1,6 @@ package com.youlai.boot.system.controller; import cn.idev.excel.EasyExcel; -import cn.idev.excel.ExcelWriter; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.framework.web.repeatsubmit.RepeatSubmit; import com.youlai.boot.common.enums.ActionTypeEnum; @@ -51,11 +50,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.List; /** @@ -166,18 +164,16 @@ public class UserController { @Operation(summary = "用户导入模板下载") @GetMapping("/template") - public void downloadTemplate(HttpServletResponse response) { + public void downloadTemplate(HttpServletResponse response) { String fileName = "用户导入模板.xlsx"; response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); - // ClassLoader 只认 / 分隔,用 File.separator 打 jar 后(Linux)会取不到资源 - String fileClassPath = "excel/" + fileName; - InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileClassPath); - - try (ServletOutputStream outputStream = response.getOutputStream(); - ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(inputStream).build()) { - excelWriter.finish(); + // 表头取自 UserImportForm 的 @ExcelProperty,写入空列表即只产出表头 + try (ServletOutputStream outputStream = response.getOutputStream()) { + EasyExcel.write(outputStream, UserImportForm.class) + .sheet("用户导入") + .doWrite(Collections.emptyList()); } catch (IOException e) { throw new BusinessException("用户导入模板下载失败", e); } diff --git a/src/main/java/com/youlai/boot/system/schedule/XxlJobSample.java b/src/main/java/com/youlai/boot/system/schedule/XxlJobSample.java index 52eb68fe..baa2db31 100644 --- a/src/main/java/com/youlai/boot/system/schedule/XxlJobSample.java +++ b/src/main/java/com/youlai/boot/system/schedule/XxlJobSample.java @@ -5,7 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** - * XXL-JOB 示例任务。 + * XXL-JOB 示例任务 * * @author Ray.Hao * @since 3.1.0 diff --git a/src/main/java/com/youlai/boot/system/service/UserImportReader.java b/src/main/java/com/youlai/boot/system/service/UserImportReader.java index c5ece4ec..f1e421d8 100644 --- a/src/main/java/com/youlai/boot/system/service/UserImportReader.java +++ b/src/main/java/com/youlai/boot/system/service/UserImportReader.java @@ -27,7 +27,7 @@ import java.util.List; import java.util.stream.Collectors; /** - * 用户导入读取器。 + * 用户导入读取器 * * @author Ray * @since 2022/4/10 diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 5b09a787..0b4199d7 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,74 +1,10 @@ { "properties": [ - { - "name": "storage.type", - "type": "java.lang.String", - "description": "文件存储类型:s3 | aliyun | local" - }, - { - "name": "storage.upload.max-file-size", - "type": "java.lang.String", - "description": "单文件大小上限(如 50MB),同时作为 spring.servlet.multipart 的上限" - }, - { - "name": "storage.upload.allowed-extensions", - "type": "java.util.List", - "description": "允许的文件扩展名白名单(置空表示不限制)" - }, - { - "name": "storage.s3.endpoint", - "type": "java.lang.String", - "description": "S3 服务 Endpoint(S3 协议兼容存储,默认部署 RustFS)" - }, - { - "name": "storage.s3.access-key", - "type": "java.lang.String", - "description": "S3 访问凭据" - }, - { - "name": "storage.s3.secret-key", - "type": "java.lang.String", - "description": "S3 凭据密钥" - }, - { - "name": "storage.s3.bucket", - "type": "java.lang.String", - "description": "S3 存储桶名称" - }, - { - "name": "storage.s3.domain", - "type": "java.lang.String", - "description": "S3 自定义域名(文件 URL 域名)" - }, - { - "name": "storage.aliyun.endpoint", - "type": "java.lang.String", - "description": "阿里云 OSS 服务 Endpoint" - }, - { - "name": "storage.aliyun.access-key-id", - "type": "java.lang.String", - "description": "阿里云 OSS 访问凭据 ID" - }, - { - "name": "storage.aliyun.access-key-secret", - "type": "java.lang.String", - "description": "阿里云 OSS 凭据密钥" - }, - { - "name": "storage.aliyun.bucket", - "type": "java.lang.String", - "description": "阿里云 OSS 存储桶名称" - }, - { - "name": "storage.local.path", - "type": "java.lang.String", - "description": "本地存储根目录(相对路径基于工作目录)" - }, { "name": "xxl.job.enabled", "type": "java.lang.Boolean", - "description": "定时任务开关" + "description": "定时任务开关", + "defaultValue": false }, { "name": "xxl.job.admin.addresses", @@ -88,47 +24,74 @@ { "name": "xxl.job.executor.address", "type": "java.lang.String", - "description": "执行器地址" + "description": "执行器地址", + "defaultValue": "" }, { "name": "xxl.job.executor.ip", "type": "java.lang.String", - "description": "执行器IP" + "description": "执行器IP", + "defaultValue": "" }, { "name": "xxl.job.executor.port", "type": "java.lang.Integer", - "description": "执行器端口" + "description": "执行器端口", + "defaultValue": 9999 }, { "name": "xxl.job.executor.logpath", "type": "java.lang.String", - "description": "日志路径" + "description": "日志路径", + "defaultValue": "/data/applogs/xxl-job/jobhandler" }, { "name": "xxl.job.executor.logretentiondays", "type": "java.lang.Integer", - "description": "日志保留天数" + "description": "日志保留天数", + "defaultValue": 30 }, { - "name": "spring.cache.enabled", - "type": "java.lang.Boolean", - "description": "缓存开关" + "name": "security.token.access-token-time-to-live", + "defaultValue": "2h" }, { - "name": "security.redis-token.multi-login", - "type": "java.lang.String", - "description": "是否允许多点登录" + "name": "security.token.refresh-token-time-to-live", + "defaultValue": "7d" + } + ], + "ignored": { + "properties": [ + { + "name": "storage.allowed-extensions" + } + ] + }, + "hints": [ + { + "name": "storage.type", + "values": [ + { + "value": "s3" + }, + { + "value": "aliyun" + }, + { + "value": "local" + } + ] }, { - "name": "security.redis-token.access-token-time-to-live", - "type": "java.lang.String", - "description": "访问令牌有效期" - }, - { - "name": "security.redis-token.refresh-token-time-to-live", - "type": "java.lang.String", - "description": "刷新令牌有效期" + "name": "security.token.type", + "values": [ + { + "value": "jwt" + }, + { + "value": "redis-token" + } + ] } ] -} \ No newline at end of file +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 4c707145..78f060cd 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,13 +1,18 @@ +# 开发环境配置 + +# 基础服务 server: port: 8000 - forward-headers-strategy: framework + # 经 Nginx 等代理访问且开启 OIDC 时取消注释 + # forward-headers-strategy: framework spring: + # MySQL 数据库 datasource: - type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true username: youlai password: 123456 + # Redis(登录会话、验证码、限流等) data: redis: database: 0 @@ -15,127 +20,23 @@ spring: port: 6379 password: 123456 timeout: 10s - lettuce: - pool: - # 连接池最大连接数 默认8 ,负数表示没有限制 - max-active: 8 - # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1 - max-wait: -1 - # 连接池中的最大空闲连接 默认8 - max-idle: 8 - # 连接池中的最小空闲连接 默认0 - min-idle: 0 - cache: - enabled: false - # 缓存类型 redis、none(不使用缓存) - type: redis - # 缓存时间(单位:ms) - redis: - time-to-live: 3600000 - # 缓存null值,防止缓存穿透 - cache-null-values: true - caffeine: - spec: initialCapacity=50,maximumSize=1000,expireAfterWrite=600s - # 邮件配置 - mail: - host: smtp.youlai.tech - port: 587 - username: your-email@example.com - password: 123456 - properties: - smtp: - auth: true - starttls: - enable: true - from: youlaitech@163.com - # AI 对话(本环境无默认密钥,默认关闭;演示环境配置见 application-youlai.yml) - ai: - # 装配开关:none 关闭,openai 启用 - model: - chat: none - embedding: none - image: none - moderation: none - audio: - speech: none - transcription: none - # 启用后在此填写模型凭据与名称 - openai: - # 厂商接口地址(OpenAI 兼容根地址) - base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 - # 厂商 API 密钥(本文件入版本库,用环境变量 AI_API_KEY 提供) - api-key: ${AI_API_KEY:} - chat: - options: - model: qwen3.7-flash - # 采样温度,取值 0~1,越小输出越稳定 - temperature: 0.3 - # 百炼私有参数:推理模型的思考内容耗时可占整次调用的大头,false 关闭 - extra-body: - enable_thinking: false - servlet: - multipart: - # 单文件大小上限,单一来源绑定 storage.upload.max-file-size - max-file-size: ${storage.upload.max-file-size:50MB} - max-request-size: ${storage.upload.max-file-size:50MB} + +# SQL 日志 mybatis-plus: - mapper-locations: classpath*:/mapper/**/*.xml - global-config: - db-config: - id-type: none - # 逻辑删除对应的全局属性名(注意:须是对象属性名,不能是表字段名,如 isDeleted 而非 is_deleted,否则逻辑删除失效) - logic-delete-field: isDeleted - logic-delete-value: 1 - logic-not-delete-value: 0 configuration: - map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl -# 接口限流 -rate-limit: - default-limit: 60 - default-window: 60s - ip: - enabled: true # 开发环境开启 IP 全局限流(供限流测试) - limit: 1000 - window: 60s - -# 安全配置 +# 安全与访问 security: token: - type: jwt # 令牌策略 [jwt|redis-token] - access-token-time-to-live: 2h # 访问令牌有效期 - refresh-token-time-to-live: 7d # 会话最长有效期,刷新不延长 + type: jwt # 令牌策略:jwt / redis-token jwt: - secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # HS256,至少 32 字节 UTF-8 - allow-multi-login: true # 两种模式共用的多设备登录策略 - # 匿名放行路径,仅跳过认证与授权判断,仍走过滤器链(保留安全响应头) - permit-all-urls: - - /api/v1/auth/login # 登录接口(账号密码登录) - - /api/v1/auth/captcha # 验证码获取接口 - - /api/v1/auth/sms/** # 短信验证码接口(发送验证码、短信登录) - - /api/v1/auth/refresh-token # 刷新令牌接口 - - /api/v1/auth/wxma/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) - - /api/v1/auth/qr-code/generate # 扫码登录-生成票据 - - /api/v1/auth/qr-code/status # 扫码登录-查询状态 - - /api/v1/auth/qr-code/login # 扫码登录-换取会话令牌 - - /api/v1/forms/public/** # 公开表单接口(匿名渲染/提交,Service 校验 is_public 总闸 + IP 限流) - - /api/v1/health # 健康检查接口(供探针与限流测试) - - ${springdoc.swagger-ui.path} # Swagger UI 路径 - - /swagger-ui/** - - /v3/api-docs/** - - /webjars/** - - /favicon.ico - - /error + secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # HS256 密钥,至少 32 字节 - # OIDC 登录 - oidc: - enabled: false - issuer: http://localhost:8080/realms/youlai - client-id: youlai-admin - client-secret: "" - # 认证成功后的前端回调地址 - frontend-callback-uri: "http://localhost:3000/#/oidc/callback" +# 请求限流 +rate-limit: + # @RateLimit 未指定参数时使用的接口限流规则 + default-limit: 60 # 文件存储 storage: @@ -143,34 +44,32 @@ storage: type: s3 # 上传限制 upload: - # 单文件大小上限(同时作为 spring.servlet.multipart 的上限,单一来源) - max-file-size: 50MB - # 允许的文件扩展名白名单(置空表示不限制) + # 扩展名白名单,留空不限制 allowed-extensions: - jpg - jpeg - png - gif - # S3 对象存储(S3 协议兼容服务,默认部署 RustFS) + # S3 存储(兼容 S3 协议,默认 RustFS) s3: endpoint: http://111.229.83.153:9000 access-key: bybaddp7zyARpgNbEGKf secret-key: p9rBdQZPBIJcMH23iyFkZkXmmawbmwPlk3JLlaaj bucket: public - # 自定义域名:配置后文件 URL 走域名,留空则用 endpoint(IP)格式 + # 文件访问域名;留空时使用对象存储端点生成访问地址 domain: # 阿里云 OSS aliyun: endpoint: oss-cn-hangzhou.aliyuncs.com - access-key-id: your-access-key-id - access-key-secret: your-access-key-secret - bucket: default + access-key-id: "" + access-key-secret: "" + bucket: "" # 本地存储 local: - # 存储根目录(相对路径基于工作目录;生产建议挂载独立卷) + # 存储目录,生产建议挂载独立磁盘 path: ./uploads/ -# springdoc 配置文档: https://springdoc.org/properties.html +# 接口文档:/swagger-ui.html springdoc: swagger-ui: path: /swagger-ui.html @@ -180,8 +79,9 @@ springdoc: path: /v3/api-docs group-configs: - group: "系统管理" - paths-to-match: "/**" - # 只生成这些包下的接口文档 + paths-to-match: + - "/**" + # 只扫描这些包 packages-to-scan: - com.youlai.boot.auth.password.controller - com.youlai.boot.auth.sms.controller @@ -195,85 +95,90 @@ springdoc: - com.youlai.boot.module.workflow.controller default-flat-param-object: true -# 验证码配置 -captcha: - # 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 - type: circle - width: 120 - height: 40 - interfere-count: 2 - # 文本透明度(0.0-1.0) - text-alpha: 0.8 - # 验证码字符配置 - code: - # 验证码字符类型 math-算术|random-随机字符 - type: math - # 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数 - length: 1 - # 验证码字体 - font: - # 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif - name: SansSerif - # 字体样式 0-普通|1-粗体|2-斜体 - weight: 1 - size: 24 - # 验证码有效期(秒) - expire-seconds: 120 +--- -# 短信配置 +# 邮件、短信、小程序与定时任务(可选) + +spring: + # 邮件(可选) + mail: + host: "" # SMTP 地址,如 smtp.example.com + username: "" # SMTP 登录账号,如 noreply@example.com + password: "" # SMTP 密码或邮箱授权码,由邮件服务商提供 + from: "" # 发件人地址,如 noreply@example.com,通常与 username 一致 + +# 阿里云短信(可选) sms: - # 阿里云短信 aliyun: - accessKeyId: LTAI5tSMgfxxxxxxdiBJLyR - accessKeySecret: SoOWRqpjtS7xxxxxxZ2PZiMTJOVC - domain: dysmsapi.aliyuncs.com - regionId: cn-shanghai - signName: 有来技术 + access-key-id: "" # 在阿里云 RAM 控制台创建的 AccessKey ID + access-key-secret: "" # 与 AccessKey ID 配套的密钥 + sign-name: "" # 短信控制台审核通过的签名名称,如 示例科技 + # 模板 CODE 从短信控制台获取,格式如 SMS_123456789 templates: - register: SMS_22xxx771 - login: SMS_22xxx772 - change-mobile: SMS_22xxx773 + register: "" # 注册验证码模板(预留) + login: "" # 登录验证码模板,模板变量使用 ${code} + change-mobile: "" # 修改手机号验证码模板,模板变量使用 ${code} -# xxl-job 定时任务配置 +# 微信小程序(可选) +wx: + miniapp: + appid: "" + secret: "" + +# 定时任务(可选) xxl: job: enabled: false admin: - # 调度中心地址,多个地址用逗号分割;3.4.1 起调度中心 context-path 为 /,地址不带 /xxl-job-admin 后缀 - addresses: http://127.0.0.1:8080 - accessToken: default_token + # 调度中心地址,多个用逗号分隔 + addresses: "" + accessToken: "" executor: appname: xxl-job-executor-${spring.application.name} - address: - ip: - port: 9999 - logpath: /data/applogs/xxl-job/jobhandler - logretentiondays: 30 -# 微信小程序配置 -wx: - miniapp: - # 不同 AppID 使用不同认证源标识。 - provider: wechat-mini-main - appid: your-app-id - secret: your-app-secret +--- -# 工作流(可选扩展,开启只需将 enabled 改为 true,引擎开关自动跟随) -workflow: - enabled: false - demo: - # 演示流程自动部署开关 - enabled: true +# OIDC 统一认证(可选) + +security: + oidc: + enabled: false + # 认证中心地址,如 https://sso.example.com/realms/youlai + issuer: "" + client-id: "" + client-secret: "" + # 认证成功后的前端回跳地址,如 http://localhost:3000/#/oidc/callback + frontend-callback-uri: "" + +--- + +# AI 对话(可选) + +spring: + ai: + openai: + # 百炼 OpenAI 兼容地址;切换服务商时修改 + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + api-key: "" # 填写服务商 API 密钥 + chat: + model: qwen3.7-flash + temperature: 0.3 + # 百炼扩展参数,切换服务商时按需调整 + extra-body: + "[enable_thinking]": false + +--- + +# 工作流(可选) -# 工作流引擎 flowable: - # 流程引擎跟随业务开关 workflow.enabled + # 总开关,同时控制业务组件和引擎 process: - enabled: ${workflow.enabled:false} - # 用户与角色走系统自有体系(sys_user/sys_role),不启用 Flowable IDM 与事件注册表 + enabled: false + # 使用系统账号体系,不启用 IDM 和事件注册表 idm: enabled: false eventregistry: enabled: false - # 引擎表初始化策略,true 启动时检查并自动建/更新 ACT_* 表;可选 true/false/create/create-drop + # 开发环境启动时自动建/更新 ACT_* 表 database-schema-update: true diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 4186e980..f7b4e3a9 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,13 +1,18 @@ +# 生产环境配置 + +# 基础服务 server: port: 8000 - forward-headers-strategy: framework + # 经 Nginx 等代理访问且开启 OIDC 时取消注释 + # forward-headers-strategy: framework spring: + # MySQL 数据库 datasource: - type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true username: youlai password: 123456 + # Redis(登录会话、验证码、限流等) data: redis: database: 1 @@ -15,134 +20,24 @@ spring: port: 6379 password: 123456 timeout: 10s - lettuce: - pool: - # 连接池最大连接数 默认8 ,负数表示没有限制 - max-active: 8 - # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1 - max-wait: -1 - # 连接池中的最大空闲连接 默认8 - max-idle: 8 - # 连接池中的最小空闲连接 默认0 - min-idle: 0 - cache: - enabled: false - # 缓存类型 redis、none(不使用缓存) - type: redis - # 缓存时间(单位:ms) - redis: - time-to-live: 3600000 - # 缓存null值,防止缓存穿透 - cache-null-values: true - caffeine: - spec: initialCapacity=50,maximumSize=1000,expireAfterWrite=600s - # 邮件配置 - mail: - host: smtp.youlai.tech - port: 587 - username: your-email@example.com - password: 123456 - properties: - smtp: - auth: true - starttls: - enable: true - # 邮件发送者 - from: youlaitech@163.com - # AI 对话(本环境无默认密钥,默认关闭;演示环境配置见 application-youlai.yml) - ai: - # 装配开关:none 关闭,openai 启用 - model: - chat: none - embedding: none - image: none - moderation: none - audio: - speech: none - transcription: none - # 启用后在此填写模型凭据与名称 - openai: - # 厂商接口地址(OpenAI 兼容根地址) - base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 - # 厂商 API 密钥(本文件入版本库,用环境变量 AI_API_KEY 提供) - api-key: ${AI_API_KEY:} - chat: - options: - # 对话模型标识 - model: qwen3.7-flash - # 采样温度,取值 0~1,越小输出越稳定 - temperature: 0.3 - # 百炼私有参数:推理模型的思考内容耗时可占整次调用的大头,false 关闭 - extra-body: - enable_thinking: false - servlet: - multipart: - # 单文件大小上限,单一来源绑定 storage.upload.max-file-size - max-file-size: ${storage.upload.max-file-size:50MB} - max-request-size: ${storage.upload.max-file-size:50MB} + +# SQL 日志 mybatis-plus: - mapper-locations: classpath*:/mapper/**/*.xml - global-config: - db-config: - # 主键ID类型 - id-type: none - # 逻辑删除对应的全局属性名(注意:须是对象属性名,不能是表字段名,如 isDeleted 而非 is_deleted,否则逻辑删除失效) - logic-delete-field: isDeleted - # 逻辑删除-删除值 - logic-delete-value: 1 - # 逻辑删除-未删除值 - logic-not-delete-value: 0 configuration: - # 驼峰下划线转换 - map-underscore-to-camel-case: true - # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 排查 SQL 时,将对应 Mapper 的日志级别调为 DEBUG + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl -# 接口限流 -rate-limit: - default-limit: 10 - default-window: 60s - ip: - enabled: true - limit: 1000 # IP 全局:1000 req / 60s - window: 60s - -# 安全配置 +# 安全与访问 security: token: - type: jwt # 令牌策略 [jwt|redis-token] - access-token-time-to-live: 2h # 访问令牌有效期 - refresh-token-time-to-live: 7d # 会话最长有效期,刷新不延长 + type: jwt # 令牌策略:jwt / redis-token jwt: secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # HS256,至少 32 字节 UTF-8 - allow-multi-login: true # 两种模式共用的多设备登录策略 - # 匿名放行路径,仅跳过认证与授权判断,仍走过滤器链(保留安全响应头) - permit-all-urls: - - /api/v1/auth/login # 登录接口(账号密码登录) - - /api/v1/auth/captcha # 验证码获取接口 - - /api/v1/auth/sms/** # 短信验证码接口(发送验证码、短信登录) - - /api/v1/auth/refresh-token # 刷新令牌接口 - - /api/v1/auth/wxma/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) - - /api/v1/auth/qr-code/generate # 扫码登录-生成票据 - - /api/v1/auth/qr-code/status # 扫码登录-查询状态 - - /api/v1/auth/qr-code/login # 扫码登录-换取会话令牌 - - /api/v1/forms/public/** # 公开表单接口(匿名渲染/提交,Service 校验 is_public 总闸 + IP 限流) - - /api/v1/health # 健康检查接口(供探针与限流测试) - - ${springdoc.swagger-ui.path} # Swagger UI 路径 - - /swagger-ui/** - - /v3/api-docs/** - - /webjars/** - - /favicon.ico - - /error - # OIDC 登录 - oidc: - enabled: ${OIDC_ENABLED:false} - issuer: ${OIDC_ISSUER:} - client-id: ${OIDC_CLIENT_ID:} - client-secret: ${OIDC_CLIENT_SECRET:} - # 认证成功后的前端回调地址 - frontend-callback-uri: "${OIDC_FRONTEND_CALLBACK_URI:}" +# 请求限流 +rate-limit: + # @RateLimit 未指定参数时使用的接口限流规则 + default-limit: 10 # 文件存储 storage: @@ -150,9 +45,7 @@ storage: type: s3 # 上传限制 upload: - # 单文件大小上限(同时作为 spring.servlet.multipart 的上限,单一来源) - max-file-size: 50MB - # 允许的文件扩展名白名单(置空表示不限制) + # 允许的文件扩展名,留空不限制 allowed-extensions: - jpg - jpeg @@ -164,20 +57,20 @@ storage: access-key: rustfs-admin secret-key: rustfs-admin bucket: public - # 自定义域名:配置后文件 URL 走域名,留空则用 endpoint(IP)格式 + # 文件访问域名;留空时使用对象存储端点生成访问地址 domain: # 阿里云 OSS aliyun: endpoint: oss-cn-hangzhou.aliyuncs.com - access-key-id: your-access-key-id - access-key-secret: your-access-key-secret - bucket: default + access-key-id: "" + access-key-secret: "" + bucket: "" # 本地存储 local: # 存储根目录(相对路径基于工作目录;生产建议挂载独立卷) path: ./uploads/ -# springdoc 配置文档: https://springdoc.org/properties.html +# 接口文档:/swagger-ui.html springdoc: swagger-ui: path: /swagger-ui.html # Swagger UI 访问路径 @@ -187,7 +80,8 @@ springdoc: path: /v3/api-docs # OpenAPI JSON 地址 group-configs: # 分组配置 - group: "系统管理" # 分组名称 - paths-to-match: "/**" # 匹配的请求路径 + paths-to-match: # 匹配的请求路径 + - "/**" packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档 - com.youlai.boot.auth.password.controller - com.youlai.boot.auth.sms.controller @@ -200,92 +94,90 @@ springdoc: - com.youlai.boot.module.workflow.controller default-flat-param-object: true # 将对象参数扁平化显示在文档中 -# 验证码配置 -captcha: - # 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 - type: circle - # 验证码宽度 - width: 120 - # 验证码高度 - height: 40 - # 验证码干扰元素个数 - interfere-count: 2 - # 文本透明度(0.0-1.0) - text-alpha: 0.8 - # 验证码字符配置 - code: - # 验证码字符类型 math-算术|random-随机字符 - type: math - # 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数 - length: 1 - # 验证码字体 - font: - # 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif - name: SansSerif - # 字体样式 0-普通|1-粗体|2-斜体 - weight: 1 - # 字体大小 - size: 24 - # 验证码有效期(秒) - expire-seconds: 120 +--- -# 短信配置 +# 邮件、短信、小程序与定时任务(可选) + +spring: + # 邮件(可选) + mail: + host: "" # SMTP 地址,如 smtp.example.com + username: "" # SMTP 登录账号,如 noreply@example.com + password: "" # SMTP 密码或邮箱授权码,由邮件服务商提供 + from: "" # 发件人地址,如 noreply@example.com,通常与 username 一致 + +# 阿里云短信(可选) sms: - # 阿里云短信 aliyun: - accessKeyId: LTAI5tSMgfxxxxxxdiBJLyR - accessKeySecret: SoOWRqpjtS7xxxxxxZ2PZiMTJOVC - domain: dysmsapi.aliyuncs.com - regionId: cn-shanghai - signName: 有来技术 + access-key-id: "" # 在阿里云 RAM 控制台创建的 AccessKey ID + access-key-secret: "" # 与 AccessKey ID 配套的密钥 + sign-name: "" # 短信控制台审核通过的签名名称,如 示例科技 + # 模板 CODE 从短信控制台获取,格式如 SMS_123456789 templates: - # 注册短信验证码模板 - register: SMS_22xxx771 - # 登录短信验证码模板 - login: SMS_22xxx772 - # 修改手机号短信验证码模板 - change-mobile: SMS_22xxx773 + register: "" # 注册验证码模板(预留) + login: "" # 登录验证码模板,模板变量使用 ${code} + change-mobile: "" # 修改手机号验证码模板,模板变量使用 ${code} -# xxl-job 定时任务配置 -xxl: - job: - # 定时任务开关 - enabled: false - admin: - # 调度中心地址,多个地址用逗号分割;3.4.1 起调度中心 context-path 为 /,地址不带 /xxl-job-admin 后缀 - addresses: http://127.0.0.1:8080 - accessToken: default_token - executor: - appname: xxl-job-executor-${spring.application.name} - address: - ip: - port: 9999 - logpath: /data/applogs/xxl-job/jobhandler - logretentiondays: 30 - -# 微信小程序配置 +# 微信小程序(可选) wx: miniapp: - provider: ${WX_MINIAPP_PROVIDER:wechat-mini-main} - appid: your-app-id - secret: your-app-secret + appid: "" + secret: "" -# 工作流(可选扩展,开启只需将 enabled 改为 true,引擎开关自动跟随) -workflow: - enabled: false - demo: - # 演示流程自动部署开关 +# 定时任务(可选) +xxl: + job: enabled: false + admin: + # 调度中心地址,多个用逗号分隔 + addresses: "" + accessToken: "" + executor: + appname: xxl-job-executor-${spring.application.name} + +--- + +# OIDC 统一认证(可选) + +security: + oidc: + enabled: false + # 认证中心地址,如 https://sso.example.com/realms/youlai + issuer: "" + client-id: "" + client-secret: "" + # 认证成功后的前端回跳地址,如 http://localhost:3000/#/oidc/callback + frontend-callback-uri: "" + +--- + +# AI 对话(可选) + +spring: + ai: + openai: + # 百炼 OpenAI 兼容地址;切换服务商时修改 + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + api-key: "" # 填写服务商 API 密钥 + chat: + model: qwen3.7-flash + temperature: 0.3 + # 百炼扩展参数,切换服务商时按需调整 + extra-body: + "[enable_thinking]": false + +--- + +# 工作流(可选) -# 工作流引擎 flowable: - # 流程引擎跟随业务开关 workflow.enabled + # 总开关,同时控制业务组件和引擎 process: - enabled: ${workflow.enabled:false} - # 用户与角色走系统自有体系(sys_user/sys_role),不启用 Flowable IDM 与事件注册表 + enabled: false + # 使用系统账号体系,不启用 IDM 和事件注册表 idm: enabled: false eventregistry: enabled: false - # 引擎表初始化策略,true 启动时检查并自动建/更新 ACT_* 表;可选 true/false/create/create-drop + # 生产环境由部署流程提前建表/升级,启动时仅校验 database-schema-update: false diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 61cf8909..9e127955 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,8 +1,68 @@ +# 公共配置;连接信息与功能开关在 application-{profile}.yml 中调整 spring: application: name: youlai-boot profiles: active: dev config: - # 引入代码生成模块的配置(数据源、表前缀等),由 codegen.yml 提供 - import: classpath:codegen.yml + # 代码生成的内部默认配置 + import: + - classpath:codegen.yml + datasource: + type: com.alibaba.druid.pool.DruidDataSource + cache: + # 默认关闭应用缓存;环境配置可选择 redis 或 caffeine + type: none + redis: + time-to-live: 1h + caffeine: + spec: initialCapacity=50,maximumSize=1000,expireAfterWrite=600s + servlet: + multipart: + max-file-size: 50MB # 单个文件上限 + max-request-size: 50MB # 整个 multipart 请求上限 + ai: + model: + # 未启用的模型类型默认关闭,环境配置按需开启 + chat: none + embedding: none + image: none + moderation: none + audio: + speech: none + transcription: none + +# 数据访问 +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + global-config: + db-config: + id-type: none + # 逻辑删除字段写实体属性名(isDeleted),不是表字段名 + logic-delete-field: isDeleted + logic-delete-value: 1 + logic-not-delete-value: 0 + configuration: + map-underscore-to-camel-case: true + +# 认证过滤器使用的公开接口 +security: + # 免登录路径,仍经过安全过滤器链 + permit-all-urls: + - /api/v1/auth/login # 账号密码登录 + - /api/v1/auth/captcha # 获取验证码 + - /api/v1/auth/sms/** # 短信验证码与短信登录 + - /api/v1/auth/refresh-token # 刷新令牌 + - /api/v1/auth/wxma/** # 微信小程序登录 / 绑定手机号 + - /api/v1/auth/qr-code/generate # 扫码登录:生成票据 + - /api/v1/auth/qr-code/status # 扫码登录:查询状态 + - /api/v1/auth/qr-code/login # 扫码登录:换取令牌 + - /api/v1/forms/public/** # 公开表单接口(匿名渲染/提交,Service 校验 is_public 总闸 + IP 限流) + - /api/v1/health # 健康检查 + - /api/v1/realtime/ws # 实时连接握手,票据在拦截器校验 + - ${springdoc.swagger-ui.path:/swagger-ui.html} # Swagger UI 路径 + - /swagger-ui/** + - /v3/api-docs/** + - /webjars/** + - /favicon.ico + - /error diff --git a/src/main/resources/codegen/frontend/js/index.crud.js.vue.vm b/src/main/resources/codegen/frontend/js/index.crud.js.vue.vm index 6a5f6d94..359a7e57 100644 --- a/src/main/resources/codegen/frontend/js/index.crud.js.vue.vm +++ b/src/main/resources/codegen/frontend/js/index.crud.js.vue.vm @@ -1,372 +1,174 @@ diff --git a/src/main/resources/excel/用户导入模板.xlsx b/src/main/resources/excel/用户导入模板.xlsx deleted file mode 100644 index 611492b0a6e2402153ac3bd6db119b6abbdd2dc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12655 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 - + @@ -11,7 +11,7 @@ - + DEBUG @@ -41,7 +41,7 @@ 1GB - + INFO diff --git a/src/main/resources/workflow/expense.bpmn20.xml b/src/main/resources/workflow/expense.bpmn20.xml index ba1b7b53..a9038599 100644 --- a/src/main/resources/workflow/expense.bpmn20.xml +++ b/src/main/resources/workflow/expense.bpmn20.xml @@ -7,11 +7,16 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_expense" targetNamespace="http://youlai.tech/workflow"> - + flow1 - + flow1 + flowInitiator + + + flowInitiator flow2 @@ -31,7 +36,8 @@ flow6 - + + ${amount > 5000} @@ -42,50 +48,57 @@ + + + - + - + - + - + - + + + + + - - + + - - + + - - - + + + - - - + + + - - - + + + diff --git a/src/main/resources/workflow/leave.bpmn20.xml b/src/main/resources/workflow/leave.bpmn20.xml index 39f46a01..406200d5 100644 --- a/src/main/resources/workflow/leave.bpmn20.xml +++ b/src/main/resources/workflow/leave.bpmn20.xml @@ -7,11 +7,16 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_leave" targetNamespace="http://youlai.tech/workflow"> - + flow1 - + flow1 + flowInitiator + + + flowInitiator flow2 @@ -27,7 +32,8 @@ flow4 flow5 - + + ${days > 3} @@ -37,42 +43,49 @@ + + + - + - + - + - + + + + + - - + + - - + + - - - - + + + + - - + + diff --git a/src/main/resources/workflow/purchase.bpmn20.xml b/src/main/resources/workflow/purchase.bpmn20.xml index 5de37dcd..01a0e023 100644 --- a/src/main/resources/workflow/purchase.bpmn20.xml +++ b/src/main/resources/workflow/purchase.bpmn20.xml @@ -8,11 +8,16 @@ id="Definitions_purchase" targetNamespace="http://youlai.tech/workflow"> - + flow1 - + flow1 + flowInitiator + + + flowInitiator flow2 @@ -32,7 +37,8 @@ flow6 - + + ${amount > 10000} @@ -43,50 +49,57 @@ + + + - + - + - + - + - + + + + + - - + + - - + + - - - + + + - - - + + + - - - + + + diff --git a/src/main/resources/workflow/seal.bpmn20.xml b/src/main/resources/workflow/seal.bpmn20.xml index 443c91ce..80da9a61 100644 --- a/src/main/resources/workflow/seal.bpmn20.xml +++ b/src/main/resources/workflow/seal.bpmn20.xml @@ -6,11 +6,16 @@ xmlns:flowable="http://flowable.org/bpmn" id="Definitions_seal" targetNamespace="http://youlai.tech/workflow"> - + flow1 - + flow1 + flowInitiator + + + flowInitiator flow2 @@ -20,35 +25,43 @@ flow3 - + + + + + - + - + - + + + + + - - + + - - + + diff --git a/src/test/java/com/youlai/boot/config/CacheConfigurationTest.java b/src/test/java/com/youlai/boot/config/CacheConfigurationTest.java new file mode 100644 index 00000000..843b8611 --- /dev/null +++ b/src/test/java/com/youlai/boot/config/CacheConfigurationTest.java @@ -0,0 +1,173 @@ +package com.youlai.boot.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.cache.autoconfigure.CacheAutoConfiguration; +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * 验证真实缓存注解与 Spring Boot 管理器选择,不连接数据库或 Redis。 + */ +class CacheConfigurationTest { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(CacheAutoConfiguration.class)) + .withUserConfiguration(CacheConfig.class, RedisCacheConfig.class) + .withBean(CountingService.class); + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod", "cache-config-test"}) + void applicationDefaultsDisableCaching(String profile) { + runner.withInitializer(new ConfigDataApplicationContextInitializer()) + .withPropertyValues("spring.profiles.active=" + profile) + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(CacheManager.class); + assertThat(context.getBean(CacheManager.class)).isInstanceOf(NoOpCacheManager.class); + assertThat(context).doesNotHaveBean(RedisCacheConfig.class); + assertThat(context).doesNotHaveBean(RedisConnectionFactory.class); + assertThat(context.getEnvironment().getProperty("spring.cache.type")).isEqualTo("none"); + }); + } + + @Test + void noneRunsEveryInvocationWithoutStoringResults() { + runner.withPropertyValues("spring.cache.type=none").run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(CacheManager.class); + assertThat(context.getBean(CacheManager.class)).isInstanceOf(NoOpCacheManager.class); + var service = context.getBean(CountingService.class); + assertThat(service.load("item")).isEqualTo(1); + assertThat(service.load("item")).isEqualTo(2); + service.evict("item"); + assertThat(service.load("item")).isEqualTo(3); + }); + } + + @Test + void caffeineCachesAndEvictsThroughAnnotations() { + runner.withPropertyValues("spring.cache.type=caffeine", + "spring.cache.caffeine.spec=maximumSize=7,expireAfterWrite=1m") + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(CacheManager.class); + assertThat(context.getBean(CacheManager.class)).isInstanceOf(CaffeineCacheManager.class); + assertThat(context).doesNotHaveBean(RedisCacheConfig.class); + var service = context.getBean(CountingService.class); + assertThat(service.load("item")).isEqualTo(1); + assertThat(service.load("item")).isEqualTo(1); + service.evict("item"); + assertThat(service.load("item")).isEqualTo(2); + + var cache = (CaffeineCache) context.getBean(CacheManager.class).getCache("probe"); + assertThat(cache).isNotNull(); + assertThat(cache.getNativeCache().policy().eviction().orElseThrow().getMaximum()).isEqualTo(7); + assertThat(cache.getNativeCache().policy().expireAfterWrite().orElseThrow().getExpiresAfter()) + .isEqualTo(Duration.ofMinutes(1)); + }); + } + + @Test + void redisUsesOneManagerAndPreservesExistingStorageFormat() { + var connectionFactory = mock(RedisConnectionFactory.class); + runner.withBean(RedisConnectionFactory.class, () -> connectionFactory) + .withPropertyValues("spring.cache.type=redis", "spring.cache.redis.time-to-live=3600000") + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(CacheManager.class); + assertThat(context.getBean(CacheManager.class)).isInstanceOf(RedisCacheManager.class); + var cache = (RedisCache) context.getBean(CacheManager.class).getCache("probe"); + assertThat(cache).isNotNull(); + var config = cache.getCacheConfiguration(); + assertThat(config.getTtlFunction().getTimeToLive("key", "value")).isEqualTo(Duration.ofHours(1)); + assertThat(config.getAllowCacheNullValues()).isTrue(); + assertThat(config.usePrefix()).isTrue(); + assertThat(config.getKeyPrefixFor("probe")).isEqualTo("probe:"); + assertThat(StandardCharsets.UTF_8.decode(config.getKeySerializationPair().write("item")).toString()) + .isEqualTo("item"); + var encodedValue = config.getValueSerializationPair().write("cached-value"); + assertThat(StandardCharsets.UTF_8.decode(encodedValue.duplicate()).toString()) + .isEqualTo("\"cached-value\""); + assertThat(config.getValueSerializationPair().read(encodedValue)).isEqualTo("cached-value"); + verify(connectionFactory, never()).getConnection(); + verify(connectionFactory, never()).getClusterConnection(); + }); + } + + @Test + void redisAppliesConfiguredPrefixTtlAndNullPolicy() { + runner.withBean(RedisConnectionFactory.class, () -> mock(RedisConnectionFactory.class)) + .withPropertyValues("spring.cache.type=redis", "spring.cache.redis.time-to-live=90s", + "spring.cache.redis.key-prefix=tenant:", "spring.cache.redis.cache-null-values=false") + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(CacheManager.class); + var cache = (RedisCache) context.getBean(CacheManager.class).getCache("probe"); + assertThat(cache).isNotNull(); + var config = cache.getCacheConfiguration(); + assertThat(config.getTtlFunction().getTimeToLive("key", "value")).isEqualTo(Duration.ofSeconds(90)); + assertThat(config.getAllowCacheNullValues()).isFalse(); + assertThat(config.getKeyPrefixFor("probe")).isEqualTo("tenant:probe:"); + }); + } + + @Test + void redisPrefixCanBeDisabled() { + runner.withBean(RedisConnectionFactory.class, () -> mock(RedisConnectionFactory.class)) + .withPropertyValues("spring.cache.type=redis", "spring.cache.redis.key-prefix=tenant:", + "spring.cache.redis.use-key-prefix=false") + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(CacheManager.class); + var cache = (RedisCache) context.getBean(CacheManager.class).getCache("probe"); + assertThat(cache).isNotNull(); + assertThat(cache.getCacheConfiguration().usePrefix()).isFalse(); + }); + } + + @ParameterizedTest + @CsvSource({"none,true", "caffeine,false"}) + void legacyEnabledFlagDoesNotOverrideExplicitType(String type, boolean legacyEnabled) { + runner.withPropertyValues("spring.cache.type=" + type, "spring.cache.enabled=" + legacyEnabled) + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(CacheManager.class); + assertThat(context.getBean(CacheManager.class)) + .isInstanceOf("none".equals(type) ? NoOpCacheManager.class : CaffeineCacheManager.class); + }); + } + + @Test + void redisWithoutConnectionFactoryFailsInsteadOfSelectingAnotherProvider() { + runner.withPropertyValues("spring.cache.type=redis").run(context -> assertThat(context).hasFailed()); + } + + static class CountingService { + + private final AtomicInteger calls = new AtomicInteger(); + + @Cacheable(cacheNames = "probe", key = "#p0") + public int load(String key) { + return calls.incrementAndGet(); + } + + @CacheEvict(cacheNames = "probe", key = "#p0") + public void evict(String key) { + } + } +} diff --git a/src/test/java/com/youlai/boot/config/ConfigurationDefaultsTest.java b/src/test/java/com/youlai/boot/config/ConfigurationDefaultsTest.java new file mode 100644 index 00000000..055c09d7 --- /dev/null +++ b/src/test/java/com/youlai/boot/config/ConfigurationDefaultsTest.java @@ -0,0 +1,146 @@ +package com.youlai.boot.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.youlai.boot.auth.password.config.CaptchaConfig; +import com.youlai.boot.auth.password.config.CaptchaProperties; +import com.youlai.boot.auth.password.service.CaptchaService; +import com.youlai.boot.framework.security.config.SecurityProperties; +import com.youlai.boot.framework.web.ratelimit.RateLimitProperties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.autoconfigure.cache.CacheType; +import org.springframework.boot.cache.autoconfigure.CacheProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.data.redis.autoconfigure.DataRedisProperties; +import org.springframework.boot.servlet.autoconfigure.MultipartProperties; +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.util.unit.DataSize; + +import javax.imageio.ImageIO; +import java.io.ByteArrayInputStream; +import java.time.Duration; +import java.util.Base64; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 验证精简环境配置后,公共设置和代码默认值仍能支撑现有行为;不连接外部服务。 + */ +class ConfigurationDefaultsTest { + + @Test + @SuppressWarnings("unchecked") + void captchaWorksWithoutAnyExplicitConfiguration() { + RedisTemplate redis = mock(RedisTemplate.class); + ValueOperations values = mock(ValueOperations.class); + when(redis.opsForValue()).thenReturn(values); + + new ApplicationContextRunner() + .withUserConfiguration(CaptchaDefaultsConfiguration.class, CaptchaConfig.class) + .withBean(RedisTemplate.class, () -> redis) + .withBean(CaptchaService.class) + .run(context -> { + assertThat(context).hasNotFailed(); + var generator = context.getBean(CodeGenerator.class); + assertThat(generator).isInstanceOf(MathGenerator.class); + assertThat(generator.verify("1+2=", "3")).isTrue(); + + var captcha = context.getBean(CaptchaService.class).generate(); + assertThat(captcha.getCaptchaId()).isNotBlank(); + assertThat(captcha.getCaptchaBase64()).startsWith("data:image/png;base64,"); + var imageBytes = Base64.getDecoder().decode( + captcha.getCaptchaBase64().substring(captcha.getCaptchaBase64().indexOf(',') + 1)); + var image = ImageIO.read(new ByteArrayInputStream(imageBytes)); + assertThat(image).isNotNull(); + assertThat(image.getWidth()).isEqualTo(120); + assertThat(image.getHeight()).isEqualTo(40); + verify(values).set(anyString(), anyString(), eq(120L), eq(TimeUnit.SECONDS)); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod", "configuration-defaults-test"}) + void profilesInheritOperationalDefaults(String profile) { + contextRunner(profile).run(context -> { + assertThat(context).hasNotFailed(); + var binder = Binder.get(context.getEnvironment()); + + var rateLimit = binder.bindOrCreate("rate-limit", RateLimitProperties.class); + assertThat(rateLimit.getDefaultLimit()).isEqualTo(switch (profile) { + case "dev" -> 60; + case "prod" -> 10; + default -> 5; + }); + assertThat(rateLimit.getDefaultWindow()).isEqualTo(Duration.ofSeconds(60)); + assertThat(rateLimit.getIp().isEnabled()).isTrue(); + assertThat(rateLimit.getIp().getLimit()).isEqualTo(1000); + assertThat(rateLimit.getIp().getWindow()).isEqualTo(Duration.ofSeconds(60)); + + var security = binder.bindOrCreate("security", SecurityProperties.class); + assertThat(security.getToken().getAccessTokenTimeToLive()).isEqualTo(Duration.ofHours(2)); + assertThat(security.getToken().getRefreshTokenTimeToLive()).isEqualTo(Duration.ofDays(7)); + assertThat(security.getToken().isAllowMultiLogin()).isTrue(); + assertThat(security.getPermitAllUrls()) + .contains("/api/v1/auth/login", "/api/v1/auth/captcha", "/swagger-ui.html") + .allSatisfy(url -> assertThat(url).doesNotContain("${")); + + var pool = binder.bindOrCreate("spring.data.redis", DataRedisProperties.class).getLettuce().getPool(); + assertThat(pool.getMaxActive()).isEqualTo(8); + assertThat(pool.getMaxIdle()).isEqualTo(8); + assertThat(pool.getMinIdle()).isZero(); + assertThat(pool.getMaxWait()).isEqualTo(Duration.ofMillis(-1)); + + var multipart = binder.bindOrCreate("spring.servlet.multipart", MultipartProperties.class); + assertThat(multipart.getMaxFileSize()).isEqualTo(DataSize.ofMegabytes(50)); + assertThat(multipart.getMaxRequestSize()).isEqualTo(DataSize.ofMegabytes(50)); + + var cache = binder.bindOrCreate("spring.cache", CacheProperties.class); + assertThat(cache.getType()).isEqualTo(CacheType.NONE); + assertThat(cache.getRedis().getTimeToLive()).isEqualTo(Duration.ofHours(1)); + assertThat(cache.getRedis().isCacheNullValues()).isTrue(); + var localCache = Caffeine.from(cache.getCaffeine().getSpec()).build(); + assertThat(localCache.policy().eviction().orElseThrow().getMaximum()).isEqualTo(1000); + assertThat(localCache.policy().expireAfterWrite().orElseThrow().getExpiresAfter()) + .isEqualTo(Duration.ofMinutes(10)); + }); + } + + @Test + void multipartLimitsCanBeOverriddenIndependently() { + contextRunner("dev") + .withPropertyValues("spring.servlet.multipart.max-file-size=7MB", + "spring.servlet.multipart.max-request-size=8MB") + .run(context -> { + assertThat(context).hasNotFailed(); + var multipart = Binder.get(context.getEnvironment()) + .bindOrCreate("spring.servlet.multipart", MultipartProperties.class); + assertThat(multipart.getMaxFileSize()).isEqualTo(DataSize.ofMegabytes(7)); + assertThat(multipart.getMaxRequestSize()).isEqualTo(DataSize.ofMegabytes(8)); + }); + } + + private ApplicationContextRunner contextRunner(String profile) { + return new ApplicationContextRunner() + .withInitializer(new ConfigDataApplicationContextInitializer()) + .withPropertyValues("spring.profiles.active=" + profile); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(CaptchaProperties.class) + static class CaptchaDefaultsConfiguration { + } +} diff --git a/src/test/java/com/youlai/boot/config/OptionalIntegrationConfigurationTest.java b/src/test/java/com/youlai/boot/config/OptionalIntegrationConfigurationTest.java new file mode 100644 index 00000000..49f8699f --- /dev/null +++ b/src/test/java/com/youlai/boot/config/OptionalIntegrationConfigurationTest.java @@ -0,0 +1,127 @@ +package com.youlai.boot.config; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import com.youlai.boot.auth.oidc.config.OidcProperties; +import com.youlai.boot.auth.wxma.config.WxMaConfig; +import com.youlai.boot.auth.wxma.config.WxMaProperties; +import com.youlai.boot.support.mail.MailConfig; +import com.youlai.boot.support.mail.MailProperties; +import com.youlai.boot.support.sms.AliyunSmsProperties; +import com.youlai.boot.support.sms.service.impl.AliyunSmsServiceImpl; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.env.SystemEnvironmentPropertySource; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 读取真实环境配置,验证空凭据和原生环境变量绑定;不发送请求或启动任务执行器。 + */ +class OptionalIntegrationConfigurationTest { + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod"}) + void optionalClientsStartWithoutCredentials(String profile) { + profileRunner(profile, Map.of()) + .withUserConfiguration(WxMaProperties.class, WxMaConfig.class, + MailProperties.class, MailConfig.class, AliyunSmsProperties.class, + AliyunSmsServiceImpl.class, OidcProperties.class, XxlJobConfig.class) + .run(context -> { + assertThat(context).hasNotFailed() + .hasSingleBean(WxMaService.class) + .hasSingleBean(JavaMailSenderImpl.class) + .hasSingleBean(AliyunSmsServiceImpl.class) + .doesNotHaveBean(OidcProperties.class) + .doesNotHaveBean(XxlJobConfig.class) + .doesNotHaveBean(XxlJobSpringExecutor.class); + + var wx = context.getBean(WxMaProperties.class); + assertThat(wx.getProvider()).isEqualTo("wechat-mini-main"); + assertThat(wx.getAppid()).isEmpty(); + assertThat(wx.getSecret()).isEmpty(); + assertThat(context.getBean(WxMaService.class).getWxMaConfig().getAppid()).isEmpty(); + + var mail = context.getBean(MailProperties.class); + assertThat(mail.getHost()).isEmpty(); + assertThat(mail.getUsername()).isEmpty(); + assertThat(mail.getPassword()).isEmpty(); + assertThat(mail.getFrom()).isEmpty(); + var sender = context.getBean(JavaMailSenderImpl.class); + assertThat(sender.getPort()).isEqualTo(587); + assertThat(sender.getJavaMailProperties()) + .containsEntry("mail.smtp.auth", true) + .containsEntry("mail.smtp.starttls.enable", true); + + var sms = context.getBean(AliyunSmsProperties.class); + assertThat(sms.getAccessKeyId()).isEmpty(); + assertThat(sms.getAccessKeySecret()).isEmpty(); + assertThat(sms.getSignName()).isEmpty(); + assertThat(sms.getDomain()).isEqualTo("dysmsapi.aliyuncs.com"); + assertThat(sms.getRegionId()).isEqualTo("cn-shanghai"); + assertThat(sms.getTemplates()).containsOnlyKeys("register", "login", "change-mobile"); + assertThat(sms.getTemplates().values()).containsOnly(""); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod"}) + void nativeEnvironmentVariablesOverridePlainYamlValues(String profile) { + profileRunner(profile, Map.of( + "SECURITY_OIDC_ENABLED", "true", + "SECURITY_OIDC_ISSUER", "https://identity.example.test/realms/demo", + "SECURITY_OIDC_CLIENTID", "test-client", + "SECURITY_OIDC_CLIENTSECRET", "test-secret", + "SECURITY_OIDC_FRONTENDCALLBACKURI", "http://localhost:3000/#/oidc/callback", + "WX_MINIAPP_PROVIDER", "wechat-mini-test")) + // 仅绑定属性,避免加载 OIDC 客户端并读取远端发现文档。 + .withUserConfiguration(OidcProperties.class, WxMaProperties.class) + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(OidcProperties.class); + var oidc = context.getBean(OidcProperties.class); + assertThat(oidc.isEnabled()).isTrue(); + assertThat(oidc.getIssuer()).isEqualTo("https://identity.example.test/realms/demo"); + assertThat(oidc.getClientId()).isEqualTo("test-client"); + assertThat(oidc.getClientSecret()).isEqualTo("test-secret"); + assertThat(oidc.getFrontendCallbackUri()).isEqualTo("http://localhost:3000/#/oidc/callback"); + assertThat(context.getBean(WxMaProperties.class).getProvider()).isEqualTo("wechat-mini-test"); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod"}) + void enablingOidcWithoutCredentialsFailsValidation(String profile) { + profileRunner(profile, Map.of()) + .withUserConfiguration(OidcProperties.class) + .withPropertyValues("security.oidc.enabled=true") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseInstanceOf(BindValidationException.class); + }); + } + + private ApplicationContextRunner profileRunner(String profile, Map environment) { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ConfigurationPropertiesAutoConfiguration.class, + ValidationAutoConfiguration.class)) + .withPropertyValues("spring.profiles.active=" + profile) + .withInitializer(context -> { + // 使用真正的系统环境属性源,同时隔离运行测试的机器所配置的第三方凭据。 + context.getEnvironment().getPropertySources().replace( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + new SystemEnvironmentPropertySource( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, environment)); + new ConfigDataApplicationContextInitializer().initialize(context); + }); + } +} diff --git a/src/test/java/com/youlai/boot/module/workflow/config/WorkflowConfigurationTest.java b/src/test/java/com/youlai/boot/module/workflow/config/WorkflowConfigurationTest.java new file mode 100644 index 00000000..ad38a9cf --- /dev/null +++ b/src/test/java/com/youlai/boot/module/workflow/config/WorkflowConfigurationTest.java @@ -0,0 +1,60 @@ +package com.youlai.boot.module.workflow.config; + +import com.youlai.boot.module.workflow.listener.ProcessCompletedListener; +import org.flowable.spring.boot.condition.ConditionalOnProcessEngine; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * 加载真实公共与环境配置,验证业务和 Flowable 的启用条件一致,不启动引擎或连接数据库 + */ +class WorkflowConfigurationTest { + + private ApplicationContextRunner contextRunner(String profile) { + return new ApplicationContextRunner() + .withInitializer(new ConfigDataApplicationContextInitializer()) + .withPropertyValues("spring.profiles.active=" + profile) + .withUserConfiguration(WorkflowEngineConfig.class, ProcessEngineConditionProbe.class) + .withBean(ProcessCompletedListener.class, () -> mock(ProcessCompletedListener.class)); + } + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod"}) + void workflowAndEngineAreDisabledByDefault(String profile) { + contextRunner(profile).run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context).doesNotHaveBean(WorkflowEngineConfig.class); + assertThat(context).doesNotHaveBean(ProcessEngineConditionProbe.class); + assertThat(context.getEnvironment().getProperty("flowable.process.enabled", Boolean.class)).isFalse(); + assertThat(context.getEnvironment().getProperty("flowable.idm.enabled", Boolean.class)).isFalse(); + assertThat(context.getEnvironment().getProperty("flowable.eventregistry.enabled", Boolean.class)).isFalse(); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod"}) + void oneWorkflowSwitchEnablesBusinessAndEngineConditions(String profile) { + contextRunner(profile).withPropertyValues("flowable.process.enabled=true").run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context).hasSingleBean(WorkflowEngineConfig.class); + assertThat(context).hasSingleBean(ProcessEngineConditionProbe.class); + assertThat(context.getEnvironment().getProperty("flowable.process.enabled", Boolean.class)).isTrue(); + assertThat(context.getEnvironment().getProperty("flowable.idm.enabled", Boolean.class)).isFalse(); + assertThat(context.getEnvironment().getProperty("flowable.eventregistry.enabled", Boolean.class)).isFalse(); + assertThat(context.getEnvironment().getProperty("flowable.database-schema-update")) + .isEqualTo("dev".equals(profile) ? "true" : "false"); + }); + } + + // 使用 Flowable 自己的条件注解,避免测试复制第三方开关逻辑,也不实例化真实流程引擎 + @Configuration(proxyBeanMethods = false) + @ConditionalOnProcessEngine + static class ProcessEngineConditionProbe { + } +} diff --git a/src/test/java/com/youlai/boot/support/ai/AiConfigurationTest.java b/src/test/java/com/youlai/boot/support/ai/AiConfigurationTest.java new file mode 100644 index 00000000..5b3fa150 --- /dev/null +++ b/src/test/java/com/youlai/boot/support/ai/AiConfigurationTest.java @@ -0,0 +1,120 @@ +package com.youlai.boot.support.ai; + +import com.sun.net.httpserver.HttpServer; +import com.youlai.boot.common.exception.BusinessException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiCommonProperties; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration; +import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import tools.jackson.databind.json.JsonMapper; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * 使用真实配置与本地 HTTP 桩验证 AI 装配及兼容协议,不连接数据库或外部模型服务 + */ +class AiConfigurationTest { + + private final ApplicationContextRunner runner = new ApplicationContextRunner() + .withInitializer(new ConfigDataApplicationContextInitializer()) + .withConfiguration(AutoConfigurations.of( + ToolCallingAutoConfiguration.class, + ChatClientAutoConfiguration.class, + OpenAiChatAutoConfiguration.class, + OpenAiEmbeddingAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, + OpenAiModerationAutoConfiguration.class, + OpenAiAudioSpeechAutoConfiguration.class, + OpenAiAudioTranscriptionAutoConfiguration.class)) + .withBean(AiChatService.class); + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod", "ai-default-test"}) + void disabledAiStartsWithoutCredentials(String profile) { + runner.withPropertyValues("spring.profiles.active=" + profile, "spring.ai.openai.api-key=") + .run(context -> { + assertThat(context).hasNotFailed().doesNotHaveBean(ChatModel.class); + assertThatThrownBy(() -> context.getBean(AiChatService.class).chat("system", "test")) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("spring.ai.model.chat"); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod"}) + void enabledAiBindsCurrentPropertiesAndPreservesVendorParameter(String profile) { + runner.withPropertyValues("spring.profiles.active=" + profile, + "spring.ai.model.chat=openai", "spring.ai.openai.api-key=test-key") + .run(context -> { + assertThat(context).hasNotFailed().hasSingleBean(ChatModel.class); + var options = context.getBean(OpenAiChatModel.class).getOptions(); + assertThat(options.getModel()).isEqualTo(context.getEnvironment() + .getRequiredProperty("spring.ai.openai.chat.model")); + assertThat(options.getTemperature()).isEqualTo(0.3); + assertThat(options.getExtraBody()).containsEntry("enable_thinking", false) + .doesNotContainKey("enablethinking"); + assertThat(context.getBean(OpenAiCommonProperties.class).getBaseUrl()) + .isEqualTo("https://dashscope.aliyuncs.com/compatible-mode/v1"); + }); + } + + @ParameterizedTest + @ValueSource(strings = {"dev", "prod"}) + void requestUsesOneVersionPrefixAndBooleanThinkingFlag(String profile) throws Exception { + var requestPath = new AtomicReference(); + var requestBody = new AtomicReference(); + var server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + requestPath.set(exchange.getRequestURI().getPath()); + requestBody.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)); + byte[] response = """ + {"id":"test-completion","object":"chat.completion","created":1,"model":"test-model", + "choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}], + "usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}} + """.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length); + try (var output = exchange.getResponseBody()) { + output.write(response); + } + }); + server.start(); + try { + runner.withPropertyValues("spring.profiles.active=" + profile, + "spring.ai.model.chat=openai", "spring.ai.openai.api-key=test-key", + "spring.ai.openai.base-url=http://127.0.0.1:" + server.getAddress().getPort() + + "/compatible-mode/v1") + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context.getBean(AiChatService.class).chat("system", "test")).isEqualTo("ok"); + assertThat(requestPath.get()).isEqualTo("/compatible-mode/v1/chat/completions"); + var body = JsonMapper.builder().build().readTree(requestBody.get()); + assertThat(body.has("enable_thinking")).isTrue(); + assertThat(body.get("enable_thinking").isBoolean()).isTrue(); + assertThat(body.get("enable_thinking").booleanValue()).isFalse(); + assertThat(body.has("enablethinking")).isFalse(); + assertThat(body.get("model").asText()).isEqualTo(context.getEnvironment() + .getRequiredProperty("spring.ai.openai.chat.model")); + }); + } finally { + server.stop(0); + } + } +}