diff --git a/.gitignore b/.gitignore index c327c544..27b758df 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ logs application-youlai.yml /uploadFile/ +/boot-run*.log +/boot-run*.err diff --git a/README.en.md b/README.en.md index 18138d06..5aa743a4 100644 --- a/README.en.md +++ b/README.en.md @@ -2,7 +2,7 @@ # youlai-boot youlai-boot -**Enterprise-grade permission management backend based on Spring Boot** +**Java 17 + Spring Boot 4.0.5 Enterprise-Grade Permission Management Backend** [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.5-6DB33F?logo=spring-boot)](https://spring.io/projects/spring-boot) [![JDK](https://img.shields.io/badge/JDK-17%2B-007396?logo=openjdk)](https://openjdk.org/) @@ -17,7 +17,7 @@
-[![Live Preview](https://img.shields.io/badge/Live%20Preview-2D8CF0?style=for-the-badge\&logo=google-chrome\&logoColor=white)](https://vue.youlai.tech) +[![Live Preview](https://img.shields.io/badge/Live%20Preview-2D8CF0?style=for-the-badge\&logo=google-chrome\&logoColor=white)](https://admin.youlai.tech) [![Mobile Preview](https://img.shields.io/badge/Mobile%20Preview-19BE6B?style=for-the-badge\&logo=android\&logoColor=white)](https://app.youlai.tech) [![Documentation](https://img.shields.io/badge/Documentation-8B5CF6?style=for-the-badge\&logo=gitbook\&logoColor=white)](https://www.youlai.tech/docs/server/spring-boot/) [![简体中文](https://img.shields.io/badge/简体中文-00B4D8?style=for-the-badge\&logo=google-translate\&logoColor=white)](./README.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..08dddafe 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@
-# youlai-boot youlai-boot +# youlai-boot youlai-boot -**Spring Boot 企业级权限管理系统后端** +**Java 17 + Spring Boot 4.0.5 企业级权限管理系统后端** [![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.5-6DB33F?logo=spring-boot)](https://spring.io/projects/spring-boot) [![JDK](https://img.shields.io/badge/JDK-17%2B-007396?logo=openjdk)](https://openjdk.org/) @@ -17,7 +17,7 @@
-[![在线预览](https://img.shields.io/badge/在线预览-2D8CF0?style=for-the-badge\&logo=google-chrome\&logoColor=white)](https://vue.youlai.tech) +[![在线预览](https://img.shields.io/badge/在线预览-2D8CF0?style=for-the-badge\&logo=google-chrome\&logoColor=white)](https://admin.youlai.tech) [![移动端预览](https://img.shields.io/badge/移动端预览-19BE6B?style=for-the-badge\&logo=android\&logoColor=white)](https://app.youlai.tech) [![阅读文档](https://img.shields.io/badge/阅读文档-8B5CF6?style=for-the-badge\&logo=gitbook\&logoColor=white)](https://www.youlai.tech/docs/server/spring-boot/) [![English](https://img.shields.io/badge/English-00B4D8?style=for-the-badge\&logo=google-translate\&logoColor=white)](./README.en.md) @@ -30,7 +30,7 @@ ## 核心特性 -- 🔐 **安全体系** — Spring Security + JWT/Redis Token 双会话模式、令牌续期、多端互斥 +- 🔐 **安全体系** — Spring Security + JWT/Redis Token 两种令牌格式、统一会话管理、令牌续期、多端互斥 - 🔑 **统一认证(可选,默认关闭)** — [OIDC 登录](../docs/youlai-boot/oidc/implementation.md),把登录交给外部认证中心,认证成功后关联本地用户并复用角色权限;不启用就不会加载相关组件 @@ -98,11 +98,7 @@ sql/extensions/oidc.sql # 统一认证:应用中心菜单与演示身份关联 ``` - > 扩展脚本全部**可选**。用户、角色、菜单、字典、日志、代码生成等基础能力都在 `sql/youlai-admin.sql` 里,只导入基础库就能把系统跑起来。 - > - > `oidc.sql` 面向"已经有认证中心"的场景,它只写入「应用中心」菜单和一条演示用的身份关联。单点登录本身默认关闭(`security.oidc.enabled` 与前端 `VITE_OIDC_ENABLED`),不填认证中心地址、客户端密钥和前端开关就不会加载 OIDC 组件,对基础功能零影响。暂时没有单点登录需求,跳过它即可。 - -3. 修改配置(可选,默认已配置线上只读数据源): +3. 检查开发配置,按实际环境填写 MySQL、Redis 连接信息: ```bash src/main/resources/application-dev.yml @@ -190,7 +186,7 @@ youlai-boot/ | [youlai-think](https://gitee.com/youlaiorg/youlai-think) | ThinkPHP + ThinkORM | PHP | ✅️ | | [youlai-aspnet](https://gitee.com/youlaiorg/youlai-aspnet) | ASP.NET Core + EF Core | C# | ✅️ | | [youlai-axum](https://gitee.com/youlaiorg/youlai-axum) | Axum + SeaORM | Rust | ✅️ | -| [youlai-laravel](https://gitee.com/youlaiorg/youlai-laravel) | Laravel + Eloquent | PHP | ❌️ | +| [youlai-laravel](https://gitee.com/youlaiorg/youlai-laravel) | Laravel + Eloquent | PHP | ✅️ | > 九种后端共享同一套 **RESTful API 规范** 和 **数据库结构**,前端可无缝切换。 @@ -203,34 +199,50 @@ youlai-boot/ | [youlai-boot (db-pg)](https://gitee.com/youlaiorg/youlai-boot/tree/db-pg) | youlai-boot | 分支 | PostgreSQL 数据库分支 | ✅️ | | [youlai-boot (multi-module)](https://gitee.com/youlaiorg/youlai-boot/tree/multi-module) | youlai-boot | 分支 | 多模块工程拆分 | ✅️ | | [youlai-nest (multi-tenant)](https://gitee.com/youlaiorg/youlai-nest/tree/multi-tenant) | youlai-nest | 分支 | 多租户 SaaS,租户隔离与租户配置 | ✅️ | -| [youlai-boot (spring-boot-3)](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) | youlai-boot | 分支 | Spring Boot 3 兼容分支 | ❌️ | +| [youlai-boot (spring-boot-3)](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) | youlai-boot | 分支 | Spring Boot 3 兼容分支 | ✅️ | ## 可选扩展 -**工作流(默认关闭)** +以下功能默认关闭,需要时再开启。 -两步开启: +**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 ebfa2af6..3b147dff 100644 Binary files a/docs/images/logo/logo.png and b/docs/images/logo/logo.png differ diff --git a/docs/images/qrcode/wechat-mp.jpg b/docs/images/qrcode/wechat-mp.jpg index 34852141..b97fd083 100644 Binary files a/docs/images/qrcode/wechat-mp.jpg and b/docs/images/qrcode/wechat-mp.jpg differ diff --git a/docs/images/qrcode/wechat-official.jpg b/docs/images/qrcode/wechat-official.jpg index 576f210a..507409ff 100644 Binary files a/docs/images/qrcode/wechat-official.jpg and b/docs/images/qrcode/wechat-official.jpg differ diff --git a/pom.xml b/pom.xml index 064270e0..36334d67 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 构建的权限管理系统。 @@ -71,7 +71,7 @@ 1.2.5 - 2.0.0 + 2.0.2 @@ -133,7 +133,7 @@ test - + org.springframework.boot spring-boot-starter-webmvc-test @@ -147,7 +147,7 @@ test - + com.h2database h2 @@ -241,7 +241,7 @@ spring-boot-starter-cache - + org.springframework.boot spring-boot-starter-aspectj @@ -252,7 +252,7 @@ spring-boot-starter-validation - + org.springframework.ai spring-ai-starter-model-openai @@ -320,7 +320,7 @@ ${xxl-job.version} - + cn.idev.excel fastexcel @@ -440,6 +440,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/controller/CodegenController.java b/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java index 77d4433c..abaf25b3 100644 --- a/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java +++ b/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java @@ -85,7 +85,7 @@ public class CodegenController { @Operation(summary = "获取预览生成代码") @GetMapping("/{tableName}/preview") public Result> getTablePreviewData(@PathVariable String tableName, - @RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType, + @RequestParam(value = "pageType", required = false, defaultValue = "crud") String pageType, @RequestParam(value = "type", required = false, defaultValue = "ts") String type) { List list = codegenService.getCodegenPreviewData(tableName, pageType, type); return Result.success(list); @@ -95,7 +95,7 @@ public class CodegenController { @GetMapping("/{tableName}/download") @Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.DOWNLOAD) public void downloadZip(HttpServletResponse response, @PathVariable String tableName, - @RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType, + @RequestParam(value = "pageType", required = false, defaultValue = "crud") String pageType, @RequestParam(value = "type", required = false, defaultValue = "ts") String type) { String[] tableNames = tableName.split(","); byte[] data = codegenService.downloadCode(tableNames, pageType, type); 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/model/form/GenConfigForm.java b/src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java index db1ab797..c37f9447 100644 --- a/src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java +++ b/src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java @@ -60,7 +60,7 @@ public class GenConfigForm { @Schema(description = "前端应用名") private String frontendAppName; - @Schema(description = "页面类型 classic|crud", example = "classic") + @Schema(description = "页面类型 classic|crud", example = "crud") private String pageType; @Schema(description = "要移除的表前缀,如: sys_", example = "sys_") 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..35dfa217 100644 --- a/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java +++ b/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java @@ -2,6 +2,7 @@ package com.youlai.boot.codegen.property; import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -21,22 +22,37 @@ import java.util.Map; public class CodegenProperties { - // 默认配置 - private DefaultConfig defaultConfig ; + /** 默认配置 */ + private DefaultConfig defaultConfig; - // 模板配置 + /** 模板配置 */ private Map templateConfigs = MapUtil.newHashMap(true); - // 后端应用名 + /** 模块名 → 权限前缀,未配置时用模块名 */ + private Map permissionNamespaces = MapUtil.newHashMap(true); + + /** + * 解析权限标识前缀 + * + * @param moduleName 模块名 + * @param entityName 实体名 + * @return 权限标识前缀,如 sys:user + */ + public String resolvePermissionPrefix(String moduleName, String entityName) { + String namespace = permissionNamespaces.getOrDefault(moduleName, moduleName); + return namespace + ":" + StrUtil.toSymbolCase(entityName, '-'); + } + + /** 后端应用名 */ private String backendAppName; - // 前端应用名 + /** 前端应用名 */ private String frontendAppName; - // 下载文件名 + /** 下载文件名 */ private String downloadFileName; - // 排除数据表 + /** 排除数据表 */ private List excludeTables; /** @@ -45,13 +61,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 +78,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/codegen/service/impl/CodegenServiceImpl.java b/src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java index f4513a5d..feb09d75 100644 --- a/src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java +++ b/src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java @@ -16,6 +16,7 @@ import com.youlai.boot.codegen.property.CodegenProperties; import com.youlai.boot.codegen.service.GenTableService; import com.youlai.boot.codegen.service.GenTableColumnService; import com.youlai.boot.codegen.service.CodegenService; +import com.youlai.boot.codegen.util.NamingUtils; import com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.codegen.mapper.DatabaseMapper; import com.youlai.boot.codegen.model.entity.GenTable; @@ -46,6 +47,9 @@ import java.util.zip.ZipOutputStream; @Slf4j public class CodegenServiceImpl implements CodegenService { + private static final List FEATURES = List.of( + "LocalDate", "LocalDateTime", "BigDecimal", "NotBlank", "NotNull", "Size"); + private final DatabaseMapper databaseMapper; private final CodegenProperties codegenProperties; private final GenTableService genTableService; @@ -247,14 +251,14 @@ public class CodegenServiceImpl implements CodegenService { + File.separator + "src" + File.separator + "api" + File.separator + moduleName - + File.separator + StrUtil.toSymbolCase(entityName, '-') + + File.separator + NamingUtils.kebab(entityName) ); } else if ("API_TYPES".equals(templateName)) { path = (codegenProperties.getFrontendAppName() + File.separator + "src" + File.separator + "api" + File.separator + moduleName - + File.separator + StrUtil.toSymbolCase(entityName, '-') + + File.separator + NamingUtils.kebab(entityName) ); } else if ("VIEW".equals(templateName)) { // path = "src/views/system/user"; @@ -262,7 +266,7 @@ public class CodegenServiceImpl implements CodegenService { + File.separator + "src" + File.separator + subPackageName + File.separator + moduleName - + File.separator + StrUtil.toSymbolCase(entityName, '-') + + File.separator + NamingUtils.kebab(entityName) ); } else { path = (codegenProperties.getBackendAppName() @@ -297,8 +301,8 @@ public class CodegenServiceImpl implements CodegenService { String pageType) { Map bindMap = new HashMap<>(); - String entityName = genTable.getEntityName(); + String entityKebab = NamingUtils.kebab(entityName); bindMap.put("packageName", genTable.getPackageName()); bindMap.put("moduleName", genTable.getModuleName()); @@ -307,65 +311,41 @@ public class CodegenServiceImpl implements CodegenService { bindMap.put("entityName", entityName); bindMap.put("tableName", genTable.getTableName()); bindMap.put("author", genTable.getAuthor()); - String entityLowerCamel = StrUtil.lowerFirst(entityName); - String entityKebab = StrUtil.toSymbolCase(entityName, '-'); - String entityUpperSnake = StrUtil.toSymbolCase(entityName, '_').toUpperCase(); - bindMap.put("entityLowerCamel", entityLowerCamel); + bindMap.put("entityLowerCamel", StrUtil.lowerFirst(entityName)); bindMap.put("entityKebab", entityKebab); - bindMap.put("entityUpperSnake", entityUpperSnake); + bindMap.put("entityKebabPlural", NamingUtils.pluralKebab(entityKebab)); + bindMap.put("entityUpperSnake", NamingUtils.upperSnake(entityName)); + bindMap.put("permissionPrefix", + codegenProperties.resolvePermissionPrefix(genTable.getModuleName(), entityName)); bindMap.put("businessName", genTable.getBusinessName()); - bindMap.put("entityComment", genTable.getBusinessName()); bindMap.put("fieldConfigs", fieldConfigs); - boolean hasLocalDate = false; - boolean hasLocalDateTime = false; - boolean hasBigDecimal = false; - boolean hasNotBlank = false; - boolean hasNotNull = false; - boolean hasSize = false; - + Set features = new HashSet<>(); for (GenTableColumn fieldConfig : fieldConfigs) { - if (StrUtil.isBlank(fieldConfig.getFieldType())) { fieldConfig.setFieldType(JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType())); } - - if ("LocalDate".equals(fieldConfig.getFieldType())) { - hasLocalDate = true; - } - if ("LocalDateTime".equals(fieldConfig.getFieldType())) { - hasLocalDateTime = true; - } - if ("BigDecimal".equals(fieldConfig.getFieldType())) { - hasBigDecimal = true; - } + String fieldType = fieldConfig.getFieldType(); + features.add(fieldType); if (ObjectUtil.equals(fieldConfig.getIsShowInForm(), 1) && !"id".equals(fieldConfig.getFieldName())) { - boolean isString = "String".equals(fieldConfig.getFieldType()); + boolean isString = "String".equals(fieldType); if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { - if (isString) { - hasNotBlank = true; - } else { - hasNotNull = true; - } + features.add(isString ? "NotBlank" : "NotNull"); } if (isString && fieldConfig.getMaxLength() != null && fieldConfig.getMaxLength() > 0) { - hasSize = true; + features.add("Size"); } } - fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldType)); + } + for (String feature : FEATURES) { + bindMap.put("has" + feature, features.contains(feature)); } - bindMap.put("hasLocalDate", hasLocalDate); - bindMap.put("hasLocalDateTime", hasLocalDateTime); - bindMap.put("hasBigDecimal", hasBigDecimal); - bindMap.put("hasNotBlank", hasNotBlank); - bindMap.put("hasNotNull", hasNotNull); - bindMap.put("hasSize", hasSize); - TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("codegen", TemplateConfig.ResourceMode.CLASSPATH)); - // 根据 ui 选择不同的前端页面模板:默认 index.vue.vm;封装版使用 index.crud.vue.vm + // classic 为兼容模板,其余情况统一使用 CrudPage 页面 String path = templatePath; - if ("crud".equalsIgnoreCase(pageType)) { + if (!"classic".equalsIgnoreCase(pageType)) { if (path.endsWith("index.js.vue.vm")) { path = path.replace("index.js.vue.vm", "index.crud.js.vue.vm"); } else if (path.endsWith("index.vue.vm")) { diff --git a/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java b/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java index 737149f8..4d9a7ddf 100644 --- a/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java +++ b/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java @@ -198,16 +198,21 @@ public class GenTableServiceImpl extends ServiceImpl i @Override public void saveGenConfig(GenConfigForm formData) { GenTable genTable = codegenConverter.toGenTable(formData); + if (StrUtil.isBlank(genTable.getPageType())) { + genTable.setPageType("crud"); + } this.saveOrUpdate(genTable); // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 Long parentMenuId = formData.getParentMenuId(); if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + String permissionPrefix = codegenProperties.resolvePermissionPrefix( + genTable.getModuleName(), genTable.getEntityName()); MenuCreateDTO dto = new MenuCreateDTO( genTable.getBusinessName(), genTable.getEntityName(), genTable.getModuleName(), - genTable.getTableName() + permissionPrefix ); menuService.addMenuForCodegen(parentMenuId, dto); } diff --git a/src/main/java/com/youlai/boot/codegen/util/NamingUtils.java b/src/main/java/com/youlai/boot/codegen/util/NamingUtils.java new file mode 100644 index 00000000..09fd819c --- /dev/null +++ b/src/main/java/com/youlai/boot/codegen/util/NamingUtils.java @@ -0,0 +1,42 @@ +package com.youlai.boot.codegen.util; + +import cn.hutool.core.util.StrUtil; + +/** + * 命名转换,代码生成的目录、路由与权限标识使用同一套规则 + * + * @author Ray.Hao + * @since 4.14.1 + */ +public final class NamingUtils { + + private NamingUtils() { + } + + /** + * 转短横线命名,如 MenuDemo → menu-demo + */ + public static String kebab(String value) { + return StrUtil.toSymbolCase(value, '-'); + } + + /** + * 转大写下划线命名,如 MenuDemo → MENU_DEMO + */ + public static String upperSnake(String value) { + return StrUtil.toSymbolCase(value, '_').toUpperCase(); + } + + /** + * 短横线命名转英文复数,如 dict-item → dict-items、category → categories + */ + public static String pluralKebab(String kebab) { + if (kebab.matches(".*(s|x|z|ch|sh)$")) { + return kebab + "es"; + } + if (kebab.matches(".*[^aeiou]y$")) { + return kebab.substring(0, kebab.length() - 1) + "ies"; + } + return kebab + "s"; + } +} 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 d3640845..2f4f3431 100644 --- a/src/main/java/com/youlai/boot/config/SecurityConfig.java +++ b/src/main/java/com/youlai/boot/config/SecurityConfig.java @@ -72,8 +72,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(); @@ -85,6 +83,8 @@ public class SecurityConfig { requestMatcherRegistry.requestMatchers("/api/v1/open/**").permitAll(); // WebRTC 信令端点:双身份(设备签名 / client JWT)由握手拦截器自行鉴权,不走标准 HTTP 认证链 requestMatcherRegistry.requestMatchers("/ws/signal").permitAll(); + // 握手使用一次性票据;其余实时接口仍需 Bearer 认证 + requestMatcherRegistry.requestMatchers(HttpMethod.GET, "/api/v1/realtime/ws").permitAll(); // client 家属端接口:与后台管理使用同一套 TokenManager 鉴权, // 但仅允许携带 client 端令牌(clientType=client)访问,实现端与端隔离 requestMatcherRegistry.requestMatchers("/api/v1/client/**") 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 d0f1526b..76d22a26 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,16 +40,20 @@ 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; // 家属客户端访问令牌有效期,未配置时回退到 accessTokenTimeToLive @@ -57,7 +62,7 @@ public class SecurityProperties { // 家属客户端刷新令牌有效期,未配置时回退到 refreshTokenTimeToLive private Duration clientRefreshTokenTimeToLive; - // JWT 配置项(type 为 jwt 时必填) + /** JWT 签名配置,仅 jwt 模式需要 */ @Valid private JwtConfig jwt; @@ -87,6 +92,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 a10cca9f..daf15bb5 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; @@ -27,7 +29,7 @@ import java.net.URI; import java.time.LocalDateTime; /** - * 阿里云 OSS 文件存储服务。 + * 阿里云 OSS 文件存储服务 */ @Component @Slf4j @@ -37,17 +39,18 @@ 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; // 自定义访问域名(可选,如 CDN / 反向代理):配置后文件 URL 走该域名,留空则用 bucket.endpoint private String domain; + @Setter(AccessLevel.NONE) private OSS aliyunOssClient; @PostConstruct @@ -56,7 +59,7 @@ public class AliyunFileServiceImpl implements FileService { } /** - * 上传文件到阿里云 OSS,按日期分目录,返回 {@code 协议://bucket.endpoint/path} 格式的 URL。 + * 上传文件到阿里云 OSS,按日期分目录,返回 {@code 协议://bucket.endpoint/path} 格式的 URL */ @Override @SneakyThrows @@ -151,7 +154,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); @@ -162,7 +165,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/S3FileServiceImpl.java b/src/main/java/com/youlai/boot/support/file/service/impl/S3FileServiceImpl.java index 981b1ec3..5248f5b8 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 @@ -180,8 +184,8 @@ public class S3FileServiceImpl implements FileService { } /** - * 生成预签名 GET URL 并去掉 query 参数,路径直达对象。 - * 依赖桶已开匿名只读(详见 createBucketIfAbsent 的策略),签名参数在 URL 中无实际作用。 + * 生成预签名 GET URL 并去掉 query 参数,路径直达对象 + * 依赖桶已开匿名只读(详见 createBucketIfAbsent 的策略),签名参数在 URL 中无实际作用 */ private String presignedUrl(String key) { GetObjectRequest getObjectRequest = GetObjectRequest.builder() @@ -197,7 +201,7 @@ public class S3FileServiceImpl implements FileService { } /** - * 删除文件。 + * 删除文件 * * @param filePath 文件完整 URL * @return 删除成功返回 true @@ -233,7 +237,7 @@ public class S3FileServiceImpl implements FileService { } /** - * 存储桶不存在时自动创建,并设置为公开只读策略。 + * 存储桶不存在时自动创建,并设置为公开只读策略 */ @SneakyThrows private void createBucketIfAbsent(String bucket) { @@ -250,4 +254,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 c2bf7ba8..72c5bf4c 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 @@ -6,7 +6,7 @@ import com.youlai.boot.support.sms.SmsTypeEnum; import java.util.Map; /** - * 短信发送服务接口。 + * 短信发送服务接口 * * @author Ray.Hao * @since 2024/8/17 @@ -14,7 +14,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/model/dto/MenuCreateDTO.java b/src/main/java/com/youlai/boot/system/model/dto/MenuCreateDTO.java index 911de1e9..6588c372 100644 --- a/src/main/java/com/youlai/boot/system/model/dto/MenuCreateDTO.java +++ b/src/main/java/com/youlai/boot/system/model/dto/MenuCreateDTO.java @@ -17,7 +17,7 @@ public record MenuCreateDTO( /** 前端组件所属模块名,如 system */ String moduleName, - /** 按钮权限前缀使用的表名,如 sys_user */ - String tableName + /** 按钮权限标识前缀,如 sys:user */ + String permissionPrefix ) { } \ No newline at end of file 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/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java index 136ad149..0db2aa9f 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java @@ -573,7 +573,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me this.updateById(menu); // 生成 CRUD 按钮权限 - saveCrudButtons(menu.getId(), dto.moduleName() + ":" + dto.tableName().replace("_", "-"), treePath); + saveCrudButtons(menu.getId(), dto.permissionPrefix(), treePath); } } 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.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.yml b/src/main/resources/codegen.yml index ff07ff42..10f9addd 100644 --- a/src/main/resources/codegen.yml +++ b/src/main/resources/codegen.yml @@ -10,6 +10,9 @@ codegen: defaultConfig: author: youlaitech moduleName: system + # 模块名 → 权限前缀,未配置时用模块名 + permissionNamespaces: + system: sys # 排除数据表 excludeTables: - gen_table diff --git a/src/main/resources/codegen/backend/controller.java.vm b/src/main/resources/codegen/backend/controller.java.vm index 3335817a..c2c7c024 100644 --- a/src/main/resources/codegen/backend/controller.java.vm +++ b/src/main/resources/codegen/backend/controller.java.vm @@ -8,8 +8,12 @@ import ${packageName}.${moduleName}.model.form.${entityName}Form; import ${packageName}.${moduleName}.model.query.${entityName}Query; import ${packageName}.${moduleName}.model.vo.${entityName}VO; import com.baomidou.mybatisplus.core.metadata.IPage; +import ${packageName}.common.annotation.Log; +import ${packageName}.common.enums.ActionTypeEnum; +import ${packageName}.common.enums.LogModuleEnum; import ${packageName}.common.result.PageResult; import ${packageName}.common.result.Result; +import ${packageName}.framework.web.repeatsubmit.RepeatSubmit; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; @@ -26,7 +30,7 @@ import jakarta.validation.Valid; */ @Tag(name = "${businessName}接口") @RestController -@RequestMapping("/api/v1/${entityKebab}") +@RequestMapping("/api/v1/${entityKebabPlural}") @RequiredArgsConstructor public class ${entityName}Controller { @@ -34,7 +38,8 @@ public class ${entityName}Controller { @Operation(summary = "$!{businessName}分页列表") @GetMapping - @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:list')") + @PreAuthorize("@ss.hasPerm('${permissionPrefix}:list')") + @Log(module = LogModuleEnum.OTHER, value = ActionTypeEnum.LIST) public PageResult<${entityName}VO> get${entityName}Page(@Valid ${entityName}Query queryParams ) { IPage<${entityName}VO> result = ${entityLowerCamel}Service.get${entityName}Page(queryParams); return PageResult.success(result); @@ -42,7 +47,9 @@ public class ${entityName}Controller { @Operation(summary = "新增${businessName}") @PostMapping - @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:create')") + @PreAuthorize("@ss.hasPerm('${permissionPrefix}:create')") + @RepeatSubmit + @Log(module = LogModuleEnum.OTHER, value = ActionTypeEnum.INSERT) public Result save${entityName}(@RequestBody @Valid ${entityName}Form formData ) { boolean result = ${entityLowerCamel}Service.save${entityName}(formData); return Result.judge(result); @@ -50,7 +57,7 @@ public class ${entityName}Controller { @Operation(summary = "获取${businessName}表单数据") @GetMapping("/{id}/form") - @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:update')") + @PreAuthorize("@ss.hasPerm('${permissionPrefix}:update')") public Result<${entityName}Form> get${entityName}Form( @Parameter(description = "$!{businessName}ID") @PathVariable Long id ) { @@ -60,7 +67,8 @@ public class ${entityName}Controller { @Operation(summary = "修改${businessName}") @PutMapping(value = "/{id}") - @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:update')") + @PreAuthorize("@ss.hasPerm('${permissionPrefix}:update')") + @Log(module = LogModuleEnum.OTHER, value = ActionTypeEnum.UPDATE) public Result update${entityName}( @Parameter(description = "$!{businessName}ID") @PathVariable Long id, @RequestBody @Validated ${entityName}Form formData @@ -71,7 +79,8 @@ public class ${entityName}Controller { @Operation(summary = "删除${businessName}") @DeleteMapping("/{ids}") - @PreAuthorize("@ss.hasPerm('${moduleName}:${entityKebab}:delete')") + @PreAuthorize("@ss.hasPerm('${permissionPrefix}:delete')") + @Log(module = LogModuleEnum.OTHER, value = ActionTypeEnum.DELETE) public Result delete${entityName}s( @Parameter(description = "$!{businessName}ID,多个以英文逗号(,)分割") @PathVariable String ids ) { diff --git a/src/main/resources/codegen/backend/entity.java.vm b/src/main/resources/codegen/backend/entity.java.vm index e28b564d..e6a962b2 100644 --- a/src/main/resources/codegen/backend/entity.java.vm +++ b/src/main/resources/codegen/backend/entity.java.vm @@ -1,7 +1,10 @@ package ${packageName}.${moduleName}.${subpackageName}; -import lombok.Getter; -import lombok.Setter; +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; #if(${hasLocalDate}) import java.time.LocalDate; #end @@ -11,7 +14,6 @@ import java.time.LocalDateTime; #if(${hasBigDecimal}) import java.math.BigDecimal; #end -import com.baomidou.mybatisplus.annotation.TableName; import ${packageName}.common.base.BaseEntity; /** @@ -20,8 +22,8 @@ import ${packageName}.common.base.BaseEntity; * @author ${author} * @since ${date} */ -@Getter -@Setter +@Data +@EqualsAndHashCode(callSuper = true) @TableName("${tableName}") public class ${entityName} extends BaseEntity { @@ -34,6 +36,11 @@ public class ${entityName} extends BaseEntity { * ${fieldConfig.fieldComment} */ #end + #if($fieldConfig.fieldName == "createBy") + @TableField(fill = FieldFill.INSERT) + #elseif($fieldConfig.fieldName == "updateBy") + @TableField(fill = FieldFill.INSERT_UPDATE) + #end private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; #end #end diff --git a/src/main/resources/codegen/backend/mapper.xml.vm b/src/main/resources/codegen/backend/mapper.xml.vm index 69a87799..51bb564f 100644 --- a/src/main/resources/codegen/backend/mapper.xml.vm +++ b/src/main/resources/codegen/backend/mapper.xml.vm @@ -19,15 +19,28 @@ FROM ${tableName} + #if($fieldConfigs) + #set ($hasDeleted = false) + #foreach($fieldConfig in ${fieldConfigs}) + #if($fieldConfig.fieldName == "isDeleted" || $fieldConfig.columnName == "is_deleted") + #set ($hasDeleted = true) + #end + #end + #if($hasDeleted) + is_deleted = 0 + #end + #end #if($fieldConfigs) #foreach($fieldConfig in ${fieldConfigs}) #if($fieldConfig.isShowInQuery) - #if($fieldConfig.fieldType == "String") + #set ($queryType = $fieldConfig.queryType.name()) + #if($queryType == "IN" || $queryType == "BETWEEN") + + #elseif($fieldConfig.fieldType == "String") #else #end - #set ($queryType = $fieldConfig.queryType.name()) #if($queryType == "EQ") AND ${fieldConfig.columnName} = #{queryParams.${fieldConfig.fieldName}} #elseif($queryType == "LIKE") @@ -38,7 +51,7 @@ #elseif($queryType == "BETWEEN") #if($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME") - + AND ${fieldConfig.columnName} >= #{startDate} @@ -46,12 +59,19 @@ AND ${fieldConfig.columnName} <= #{endDate} - #else - + #elseif($fieldConfig.fieldType == "String") + AND ${fieldConfig.columnName} >= #{queryParams.${fieldConfig.fieldName}[0]} AND ${fieldConfig.columnName} <= #{queryParams.${fieldConfig.fieldName}[1]} + + #else + + AND ${fieldConfig.columnName} >= #{queryParams.${fieldConfig.fieldName}[0]} + + + AND ${fieldConfig.columnName} <= #{queryParams.${fieldConfig.fieldName}[1]} #end #elseif($queryType == "GT") @@ -74,6 +94,7 @@ #end #end + ORDER BY id DESC diff --git a/src/main/resources/codegen/backend/query.java.vm b/src/main/resources/codegen/backend/query.java.vm index e8e57c80..cb0e1d0a 100644 --- a/src/main/resources/codegen/backend/query.java.vm +++ b/src/main/resources/codegen/backend/query.java.vm @@ -5,24 +5,44 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; #if($fieldConfigs) - #set ($hasDateBetween = false) + #set ($needList = false) + #set ($needLocalDate = false) + #set ($needLocalDateTime = false) + #set ($needBigDecimal = false) #foreach($fieldConfig in ${fieldConfigs}) - #if(($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME") && $fieldConfig.queryType.name() == "BETWEEN") - #set ($hasDateBetween = true) + #if($fieldConfig.isShowInQuery == 1) + #set ($queryTypeName = $fieldConfig.queryType.name()) + #if($queryTypeName == "IN" || $queryTypeName == "BETWEEN") + #set ($needList = true) + #end + #if($fieldConfig.fieldType == "LocalDate") + #if(($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME") && ($queryTypeName == "IN" || $queryTypeName == "BETWEEN")) + #else + #set ($needLocalDate = true) + #end + #elseif($fieldConfig.fieldType == "LocalDateTime") + #if(($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME") && ($queryTypeName == "IN" || $queryTypeName == "BETWEEN")) + #else + #set ($needLocalDateTime = true) + #end + #end + #if($fieldConfig.fieldType == "BigDecimal") + #set ($needBigDecimal = true) + #end #end #end - #if($hasDateBetween) + #if($needList) import java.util.List; #end -#end -#if(${hasLocalDate}) + #if($needLocalDate) import java.time.LocalDate; -#end -#if(${hasLocalDateTime}) + #end + #if($needLocalDateTime) import java.time.LocalDateTime; -#end -#if(${hasBigDecimal}) + #end + #if($needBigDecimal) import java.math.BigDecimal; + #end #end /** @@ -43,13 +63,17 @@ public class ${entityName}Query extends BaseQuery { @Schema(description = "${fieldConfig.fieldComment}") #end #if($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME") - #if($fieldConfig.queryType.name() == "BETWEEN") + #if($fieldConfig.queryType.name() == "BETWEEN" || $fieldConfig.queryType.name() == "IN") private List ${fieldConfig.fieldName}; #else private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; #end #else + #if($fieldConfig.queryType.name() == "BETWEEN" || $fieldConfig.queryType.name() == "IN") + private List<${fieldConfig.fieldType}> ${fieldConfig.fieldName}; + #else private ${fieldConfig.fieldType} ${fieldConfig.fieldName}; + #end #end #end #end diff --git a/src/main/resources/codegen/frontend/js/api.js.vm b/src/main/resources/codegen/frontend/js/api.js.vm index 4b9cc527..c44c414b 100644 --- a/src/main/resources/codegen/frontend/js/api.js.vm +++ b/src/main/resources/codegen/frontend/js/api.js.vm @@ -1,9 +1,11 @@ import request from "@/utils/request"; -const ${entityUpperSnake}_BASE_URL = "/api/v1/${entityKebab}"; +const ${entityUpperSnake}_BASE_URL = "/api/v1/${entityKebabPlural}"; const ${entityName}API = { - /** 获取${businessName}分页数据 */ + /** + * 获取${businessName}分页数据 + */ getPage(queryParams) { return request({ url: `${${entityUpperSnake}_BASE_URL}`, 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..7d37e5db 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,220 @@ diff --git a/src/main/resources/codegen/frontend/js/index.js.vue.vm b/src/main/resources/codegen/frontend/js/index.js.vue.vm index 9c3f1db8..202fe413 100644 --- a/src/main/resources/codegen/frontend/js/index.js.vue.vm +++ b/src/main/resources/codegen/frontend/js/index.js.vue.vm @@ -6,7 +6,29 @@ #foreach($fieldConfig in $fieldConfigs) #if($fieldConfig.isShowInQuery == 1) - #if($fieldConfig.formType.name() == "INPUT") + #if($fieldConfig.queryType.name() == "IN") + #if($fieldConfig.dictType && $fieldConfig.dictType.trim() != "") + + #else + + #end + #elseif($fieldConfig.queryType.name() == "BETWEEN" && $fieldConfig.formType.name() != "DATE" && $fieldConfig.formType.name() != "DATE_TIME") +
+ #if($fieldConfig.tsType == "number") + + ~ + + #else + + ~ + + #end +
+ #elseif($fieldConfig.formType.name() == "INPUT")
新增