diff --git a/.gitignore b/.gitignore index 0e13eebbeaada93aed5cfa8578698f950d09e8ae..125a2c399c0b6ea1ddd9160d34ba5905cce9abb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,18 @@ -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +# Created by .ignore support plugin (hsz.mobi) +### Example sysUserDetails template template +### Example sysUserDetails template + +# IntelliJ project files +.idea +*.iml +out +gen +target +*.log +logs +.history + + +docker/*/data/ +docker/minio/config +docker/xxljob/logs \ No newline at end of file diff --git a/hypersense-access/pom.xml b/hypersense-access/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..eda25dd2f840e92694bc036a5de2c73c8d5f2049 --- /dev/null +++ b/hypersense-access/pom.xml @@ -0,0 +1,60 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + + hypersense-access + + 应用服务入口 + + + + + + tech.hypersense + hypersense-common-doc + + + + tech.hypersense + hypersense-shared-xxljob + + + + tech.hypersense + hypersense-codegen + + + + tech.hypersense + hypersense-system + + + + tech.hypersense + hypersense-common-mybatis + + + + tech.hypersense + hypersense-shared-oss + + + + tech.hypersense + hypersense-shared-hsauth + + + + com.alibaba + druid-spring-boot-starter + + + + + + + diff --git a/hypersense-access/src/main/java/tech/hypersense/HyperSenseApplication.java b/hypersense-access/src/main/java/tech/hypersense/HyperSenseApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..b6808e93744623f81f60cd2f27287c11b9a08119 --- /dev/null +++ b/hypersense-access/src/main/java/tech/hypersense/HyperSenseApplication.java @@ -0,0 +1,17 @@ +package tech.hypersense; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; + + +@SpringBootApplication +public class HyperSenseApplication { + + public static void main(String[] args) { + SpringApplication application = new SpringApplication(HyperSenseApplication.class); + application.setApplicationStartup(new BufferingApplicationStartup(2048)); + application.run(args); + System.out.println("✌(>‿◠)✌ HyperSense start complete ✌≧◔◡◔≦✌"); + } +} diff --git a/hypersense-access/src/main/java/tech/hypersense/auth/enums/CaptchaTypeEnum.java b/hypersense-access/src/main/java/tech/hypersense/auth/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..c6eb53e8a47f293e52d364798946739e0a7892c6 --- /dev/null +++ b/hypersense-access/src/main/java/tech/hypersense/auth/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.auth.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: EasyCaptcha 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-access/src/main/java/tech/hypersense/auth/model/CaptchaInfo.java b/hypersense-access/src/main/java/tech/hypersense/auth/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..274dc0f8d082160ace139d5b9837b6cd9951abac --- /dev/null +++ b/hypersense-access/src/main/java/tech/hypersense/auth/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.auth.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} \ No newline at end of file diff --git a/hypersense-access/src/main/java/tech/hypersense/auth/service/AuthService.java b/hypersense-access/src/main/java/tech/hypersense/auth/service/AuthService.java new file mode 100644 index 0000000000000000000000000000000000000000..a4d3fbbac258547c3f92e176bdd0281d45805879 --- /dev/null +++ b/hypersense-access/src/main/java/tech/hypersense/auth/service/AuthService.java @@ -0,0 +1,67 @@ +package tech.hypersense.auth.service; + +import tech.hypersense.auth.model.CaptchaInfo; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证服务接口 + * @Version: 1.0 + */ +public interface AuthService { + + /** + * 登录 + * + * @param username 用户名 + * @param password 密码 + * @return 登录结果 + */ + AuthenticationToken login(String username, String password); + + /** + * 登出 + */ + void logout(); + + /** + * 获取验证码 + * + * @return 验证码 + */ + CaptchaInfo getCaptcha(); + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AuthenticationToken refreshToken(String refreshToken); + + /** + * 微信小程序登录 + * + * @param code 微信登录code + * @return 登录结果 + */ + AuthenticationToken loginByWechat(String code); + + /** + * 发送短信验证码 + * + * @param mobile 手机号 + */ + void sendSmsLoginCode(String mobile); + + /** + * 短信验证码登录 + * + * @param mobile 手机号 + * @param code 验证码 + * @return 登录结果 + */ + AuthenticationToken loginBySms(String mobile, String code); +} + diff --git a/hypersense-access/src/main/java/tech/hypersense/auth/service/impl/AuthServiceImpl.java b/hypersense-access/src/main/java/tech/hypersense/auth/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..71de6114d352bc65d758eabc1ddcd4d9890339c9 --- /dev/null +++ b/hypersense-access/src/main/java/tech/hypersense/auth/service/impl/AuthServiceImpl.java @@ -0,0 +1,231 @@ +package tech.hypersense.auth.service.impl; + +import cn.hutool.captcha.AbstractCaptcha; +import cn.hutool.captcha.CaptchaUtil; +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import tech.hypersense.auth.enums.CaptchaTypeEnum; +import tech.hypersense.auth.model.CaptchaInfo; +import tech.hypersense.auth.service.AuthService; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.token.TokenManager; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.config.properties.CaptchaProperties; +import tech.hypersense.shared.hsauth.extension.sms.SmsAuthenticationToken; +import tech.hypersense.shared.hsauth.extension.wechat.WechatAuthenticationToken; +import tech.hypersense.shared.sms.enums.SmsTypeEnum; +import tech.hypersense.shared.sms.service.SmsService; + +import java.awt.*; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AuthServiceImpl implements AuthService { + + private final AuthenticationManager authenticationManager; + private final TokenManager tokenManager; + + private final Font captchaFont; + private final CaptchaProperties captchaProperties; + private final CodeGenerator codeGenerator; + + private final SmsService smsService; + private final RedisTemplate redisTemplate; + + /** + * 用户名密码登录 + * + * @param username 用户名 + * @param password 密码 + * @return 访问令牌 + */ + @Override + public AuthenticationToken login(String username, String password) { + // 1. 创建用于密码认证的令牌(未认证) + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(username.trim(), password); + + // 2. 执行认证(认证中) + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) + AuthenticationToken authenticationTokenResponse = + tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + return authenticationTokenResponse; + } + + /** + * 微信一键授权登录 + * + * @param code 微信登录code + * @return 访问令牌 + */ + @Override + public AuthenticationToken loginByWechat(String code) { + // 1. 创建用户微信认证的令牌(未认证) + WechatAuthenticationToken wechatAuthenticationToken = new WechatAuthenticationToken(code); + + // 2. 执行认证(认证中) + Authentication authentication = authenticationManager.authenticate(wechatAuthenticationToken); + + // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) + AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + + return authenticationToken; + } + + /** + * 发送登录短信验证码 + * + * @param mobile 手机号 + */ + @Override + public void sendSmsLoginCode(String mobile) { + + // 随机生成4位验证码 + // String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); + // TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码 + String code = "1234"; + + // 发送短信验证码 + Map templateParams = new HashMap<>(); + templateParams.put("code", code); + try { + smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams); + } catch (Exception e) { + log.error("发送短信验证码失败", e); + } + // 缓存验证码至Redis,用于登录校验 + redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES); + } + + /** + * 短信验证码登录 + * + * @param mobile 手机号 + * @param code 验证码 + * @return 访问令牌 + */ + @Override + public AuthenticationToken loginBySms(String mobile, String code) { + // 1. 创建用户短信验证码认证的令牌(未认证) + SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code); + + // 2. 执行认证(认证中) + Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken); + + // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) + AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + + return authenticationToken; + } + + /** + * 注销登录 + */ + @Override + public void logout() { + String token = SecurityUtils.getTokenFromRequest(); + if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX )) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + // 将JWT令牌加入黑名单 + tokenManager.blacklistToken(token); + // 清除Security上下文 + SecurityContextHolder.clearContext(); + } + } + + /** + * 获取验证码 + * + * @return 验证码 + */ + @Override + public CaptchaInfo getCaptcha() { + + String captchaType = captchaProperties.getType(); + int width = captchaProperties.getWidth(); + int height = captchaProperties.getHeight(); + int interfereCount = captchaProperties.getInterfereCount(); + int codeLength = captchaProperties.getCode().getLength(); + + AbstractCaptcha captcha; + if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount); + } else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength); + } else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount); + } else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) { + captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount); + } else { + throw new IllegalArgumentException("Invalid captcha type: " + captchaType); + } + captcha.setGenerator(codeGenerator); + captcha.setTextAlpha(captchaProperties.getTextAlpha()); + captcha.setFont(captchaFont); + + String captchaCode = captcha.getCode(); + String imageBase64Data = captcha.getImageBase64Data(); + + // 验证码文本缓存至Redis,用于登录校验 + String captchaKey = IdUtil.fastSimpleUUID(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaKey), + captchaCode, + captchaProperties.getExpireSeconds(), + TimeUnit.SECONDS + ); + + return CaptchaInfo.builder() + .captchaKey(captchaKey) + .captchaBase64(imageBase64Data) + .build(); + } + + /** + * 刷新token + * + * @param refreshToken 刷新令牌 + * @return 新的访问令牌 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 验证刷新令牌 + boolean isValidate = tokenManager.validateToken(refreshToken); + + if (!isValidate) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + // 刷新令牌有效,生成新的访问令牌 + return tokenManager.refreshToken(refreshToken); + } + + +} diff --git a/hypersense-access/src/main/resources/application-dev.yml b/hypersense-access/src/main/resources/application-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..3311a2ddc937fa87722d89f444b071b37eb9a6e5 --- /dev/null +++ b/hypersense-access/src/main/resources/application-dev.yml @@ -0,0 +1,245 @@ +server: + port: 8989 + +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/hypersense_blog?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true + username: hypersense + password: hypersense + data: + redis: + database: 0 + host: localhost + port: 6379 + # 如果Redis 服务未设置密码,需要将password删掉或注释,而不是设置为空字符串 +# password: 123456 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 默认8 ,负数表示没有限制 + max-active: 8 + # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1 + max-wait: -1 + # 连接池中的最大空闲连接 默认8 + max-idle: 8 + # 连接池中的最小空闲连接 默认0 + min-idle: 0 + main: + # SpringAI + banner-mode: CONSOLE + ai: + openai: + base-url: https://api.siliconflow.cn/ + api-key: sk-syrjaznwkfypnbxhvyakplapeycsuqvbenoqhggslbnlbhec + chat: + options: + model: Qwen/Qwen2.5-14B-Instruct +# anthropic: +# api-key: #这里换成你的api-key + mcp: + server: + enabled: true + name: archives-management-server + version: 1.0.0 + type: SYNC + sse-message-endpoint: /mcp/message + cache: + enabled: false + # 缓存类型 redis、none(不使用缓存) + type: redis + # 缓存时间(单位:ms) + redis: + time-to-live: 3600000 + # 缓存null值,防止缓存穿透 + cache-null-values: true + caffeine: + spec: initialCapacity=50,maximumSize=1000,expireAfterWrite=600s + # 邮件配置 + mail: + host: smtp.hypersense.tech + port: 587 + username: your-email@example.com + password: 123456 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + # 邮件发送者 + from: hypersense@yeah.net + +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + global-config: + db-config: + # 主键ID类型 + id-type: none + # 逻辑删除对应的全局属性名(注意:须是对象属性名,不能是表字段名,如 isDeleted 而非 is_deleted,否则逻辑删除失效) + logic-delete-field: isDeleted + # 逻辑删除-删除值 + logic-delete-value: 1 + # 逻辑删除-未删除值 + logic-not-delete-value: 0 + configuration: + # 驼峰下划线转换 + map-underscore-to-camel-case: true + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +# 安全配置 +security: + session: + type: jwt # 会话方式 [jwt|redis-token] + access-token-time-to-live: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期 + refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期 + jwt: + secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符) + redis-token: + allow-multi-login: false # 是否允许多设备登录 + # 安全白名单路径(完全绕过安全过滤器) + ignore-urls: + - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) + - /api/v1/auth/captcha # 验证码获取接口 + - /api/v1/auth/refresh-token # 刷新令牌接口 + - /ws/** # WebSocket接口 + - /api/v1/blog/** # 博客查询展示页 + - /api/chatbot + # 非安全端点路径(允许匿名访问的API) + unsecured-urls: + - ${springdoc.swagger-ui.path} + - /doc.html + - /swagger-ui/** + - /v3/api-docs/** + - /webjars/** + - /api/v1/blog/** # 博客查询展示页 + - /api/chatbot + +# 文件存储配置 +oss: + # OSS 类型 (目前支持aliyun、minio、local) + type: minio + # MinIO 对象存储服务 + minio: + # MinIO 服务地址 + endpoint: http://150.158.18.177:9000 + # 访问凭据 + access-key: BWyx1QgREOYrIAw6SC3K + # 凭据密钥 + secret-key: WeYdHxzDS4DksBBAjJXVcq8ns9eaTYyxsxWIJt15 + # 存储桶名称 + bucket-name: archives + # (可选) 自定义域名:配置后,文件 URL 会使用该域名格式 + custom-domain: + # 阿里云OSS对象存储服务 + aliyun: + # 服务Endpoint + endpoint: oss-cn-hangzhou.aliyuncs.com + # 访问凭据` + access-key-id: your-access-key-id + # 凭据密钥 + access-key-secret: your-access-key-secret + # 存储桶名称 + bucket-name: default + # 本地存储 + local: + # 文件存储路径 请注意下,mac用户请使用 /Users/your-username/your-path/,否则会有权限问题,windows用户请使用 D:/your-path/ + storage-path: /Users/theo/home/ +# 短信配置 +sms: + # 阿里云短信 + aliyun: + accessKeyId: LTAI5tSMgfxxxxxxdiBJLyR + accessKeySecret: SoOWRqpjtS7xxxxxxZ2PZiMTJOVC + domain: dysmsapi.aliyuncs.com + regionId: cn-shanghai + signName: 氦闪技术 + templates: + # 注册短信验证码模板 + register: SMS_22xxx771 + # 登录短信验证码模板 + login: SMS_22xxx772 + # 修改手机号短信验证码模板 + change-mobile: SMS_22xxx773 + +# springdoc配置: https://springdoc.org/properties.html +springdoc: + swagger-ui: + path: /swagger-ui.html + operations-sorter: alpha + tags-sorter: alpha + api-docs: + path: /v3/api-docs + group-configs: + - group: '系统管理' + paths-to-match: "/**" + packages-to-scan: + - com.hypersense.boot.system.controller + - com.hypersense.boot.shared.auth.controller + - com.hypersense.boot.shared.file.controller + - com.hypersense.boot.shared.codegen.controller + default-flat-param-object: true + +# knife4j 接口文档配置 +knife4j: + # 是否开启 Knife4j 增强功能 + enable: true # 设置为 true 表示开启增强功能 + # 生产环境配置 + production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用) + setting: + language: zh_cn + +# xxl-job 定时任务配置 +xxl: + job: + # 定时任务开关 + enabled: false + admin: + # 调度中心地址,多个逗号分隔 + addresses: http://150.158.18.177:8080/xxl-job-admin + accessToken: default_token + # 执行器配置 + executor: + appname: xxl-job-executor-${spring.application.name} # 执行器AppName + address: # 执行器注册地址,默认为空,多网卡时可手动设置 + ip: # 执行器IP,默认为空,多网卡时可手动设置 + port: 9999 # 执行器通讯端口 + logpath: /data/applogs/xxl-job/jobhandler # 任务运行日志文件存储磁盘路径 + logretentiondays: 30 # 日志保存天数,值大于3时生效 + +# 验证码配置 +captcha: + # 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + type: circle + # 验证码宽度 + width: 120 + # 验证码高度 + height: 40 + # 验证码干扰元素个数 + interfere-count: 2 + # 文本透明度(0.0-1.0) + text-alpha: 0.8 + # 验证码字符配置 + code: + # 验证码字符类型 math-算术|random-随机字符 + type: math + # 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数 + length: 1 + # 验证码字体 + font: + # 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif + name: SansSerif + # 字体样式 0-普通|1-粗体|2-斜体 + weight: 1 + # 字体大小 + size: 24 + # 验证码有效期(秒) + expire-seconds: 120 + +# 微信小程配置 +wx: + miniapp: + app-id: xxxxxx + app-secret: xxxxxx diff --git a/hypersense-access/src/main/resources/application-prod.yml b/hypersense-access/src/main/resources/application-prod.yml new file mode 100644 index 0000000000000000000000000000000000000000..b2ce9cd9afcc2d027fa5df1572126c1ae190bb9a --- /dev/null +++ b/hypersense-access/src/main/resources/application-prod.yml @@ -0,0 +1,221 @@ +server: + port: 8989 + +spring: + datasource: + type: com.alibaba.druid.pool.DruidDataSource + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://150.158.18.177:3306/hypersense_blog?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true + username: root + password: 123456 + data: + redis: + database: 11 + host: 150.158.18.177 + port: 6379 + password: 123456 + timeout: 10s + lettuce: + pool: + # 连接池最大连接数 默认8 ,负数表示没有限制 + max-active: 8 + # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1 + max-wait: -1 + # 连接池中的最大空闲连接 默认8 + max-idle: 8 + # 连接池中的最小空闲连接 默认0 + min-idle: 0 + cache: + enabled: false + # 缓存类型 redis、none(不使用缓存) + type: redis + # 缓存时间(单位:ms) + redis: + time-to-live: 3600000 + # 缓存null值,防止缓存穿透 + cache-null-values: true + caffeine: + spec: initialCapacity=50,maximumSize=1000,expireAfterWrite=600s + # 邮件配置 + mail: + host: smtp.youlai.tech + port: 587 + username: your-email@example.com + password: 123456 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + # 邮件发送者 + from: youlaitech@163.com +mybatis-plus: + mapper-locations: classpath*:/mapper/**/*.xml + global-config: + db-config: + # 主键ID类型 + id-type: none + # 逻辑删除对应的全局属性名(注意:须是对象属性名,不能是表字段名,如 isDeleted 而非 is_deleted,否则逻辑删除失效) + logic-delete-field: isDeleted + # 逻辑删除-删除值 + logic-delete-value: 1 + # 逻辑删除-未删除值 + logic-not-delete-value: 0 + configuration: + # 驼峰下划线转换 + map-underscore-to-camel-case: true + # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +# 安全配置 +security: + session: + type: jwt # 会话方式 [jwt|redis-token] + access-token-time-to-live: 3600 # 访问令牌 有效期(单位:秒),默认 1 小时,-1 表示永不过期 + refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期 + jwt: + secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符) + redis-token: + allow-multi-login: true # 是否允许多设备登录 + # 安全白名单路径(完全绕过安全过滤器) + ignore-urls: + - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) + - /api/v1/auth/captcha # 验证码获取接口 + - /api/v1/auth/refresh-token # 刷新令牌接口 + - /ws/** # WebSocket接口 + - /api/v1/blog/** # 博客查询展示页 + # 非安全端点路径(允许匿名访问的API) + unsecured-urls: + - ${springdoc.swagger-ui.path} + - /doc.html + - /swagger-ui/** + - /v3/api-docs/** + - /webjars/** + - /api/v1/blog/** # 博客查询展示页 + +# 文件存储配置 +oss: + # OSS 类型 (目前支持aliyun、minio) + type: minio + # MinIO 对象存储服务 + minio: + # 服务Endpoint + endpoint: http://150.158.18.177:9000 + # 访问凭据 + access-key: 69Vj5mYhO6W85n2hqwCc + # 凭据密钥 + secret-key: 9bBEo5vKtaxFyfEVL7LbwamhKCwG9KNKhkGuQDL0 + # 存储桶名称 + bucket-name: hypersenseblog + # (可选)自定义域名,如果配置了域名,生成的文件URL是域名格式,未配置则URL则是IP格式 (eg: https://oss.youlai.tech) + custom-domain: + # 阿里云OSS对象存储服务 + aliyun: + # 服务Endpoint + endpoint: oss-cn-hangzhou.aliyuncs.com + # 访问凭据 + access-key-id: your-access-key-id + # 凭据密钥 + access-key-secret: your-access-key-secret + # 存储桶名称 + bucket-name: default + # 本地存储 + local: + # 文件存储路径 请注意下,mac用户请使用 /Users/your-username/your-path/,否则会有权限问题,windows用户请使用 D:/your-path/ + storage-path: /Users/theo/home/ +# 短信配置 +sms: + # 阿里云短信 + aliyun: + accessKeyId: LTAI5tSMgfxxxxxxdiBJLyR + accessKeySecret: SoOWRqpjtS7xxxxxxZ2PZiMTJOVC + domain: dysmsapi.aliyuncs.com + regionId: cn-shanghai + signName: 有来技术 + templates: + # 注册短信验证码模板 + register: SMS_22xxx771 + # 登录短信验证码模板 + login: SMS_22xxx772 + # 修改手机号短信验证码模板 + change-mobile: SMS_22xxx773 + +# springdoc配置: https://springdoc.org/properties.html +springdoc: + swagger-ui: + path: /swagger-ui.html + operationsSorter: alpha + tags-sorter: alpha + api-docs: + path: /v3/api-docs + group-configs: + - group: '系统管理' + paths-to-match: "/**" + packages-to-scan: + - com.youlai.boot.system.controller + - com.youlai.boot.shared.auth.controller + - com.youlai.boot.shared.file.controller + - com.youlai.boot.shared.codegen.controller + default-flat-param-object: true + +# knife4j 接口文档配置 +knife4j: + # 是否开启 Knife4j 增强功能 + enable: true # 设置为 true 表示开启增强功能 + # 生产环境配置 + production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用) + setting: + language: zh_cn + +# xxl-job 定时任务配置 +xxl: + job: + # 定时任务开关 + enabled: false + admin: + # 多个地址使用,分割 + addresses: http://150.158.18.177:8686/xxl-job-admin + accessToken: default_token + executor: + appname: xxl-job-executor-${spring.application.name} + address: + ip: + port: 9999 + logpath: /data/applogs/xxl-job/jobhandler + logretentiondays: 30 + +# 验证码配置 +captcha: + # 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + type: circle + # 验证码宽度 + width: 120 + # 验证码高度 + height: 40 + # 验证码干扰元素个数 + interfere-count: 2 + # 文本透明度(0.0-1.0) + text-alpha: 0.8 + # 验证码字符配置 + code: + # 验证码字符类型 math-算术|random-随机字符 + type: math + # 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数 + length: 1 + # 验证码字体 + font: + # 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif + name: SansSerif + # 字体样式 0-普通|1-粗体|2-斜体 + weight: 1 + # 字体大小 + size: 24 + # 验证码有效期(秒) + expire-seconds: 120 + +# 微信小程配置 +wx: + miniapp: + app-id: xxxxxx + app-secret: xxxxxx \ No newline at end of file diff --git a/hypersense-access/src/main/resources/application.yml b/hypersense-access/src/main/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..16e4c2310b3d8d5ca7b361ddd02701018058a585 --- /dev/null +++ b/hypersense-access/src/main/resources/application.yml @@ -0,0 +1,11 @@ +spring: + application: + name: hypernse-boot + profiles: + active: dev + # config: + # import: classpath:codegen.yml + +# 在 banner.txt 中显示项目版本,使用 @project.version@ 从 pom.xml 获取 +project: + version: @project.version@ diff --git a/hypersense-access/src/main/resources/banner.txt b/hypersense-access/src/main/resources/banner.txt new file mode 100644 index 0000000000000000000000000000000000000000..f3c6f0a7f05f43f561d7d66a74e46817e7fd7f9b --- /dev/null +++ b/hypersense-access/src/main/resources/banner.txt @@ -0,0 +1,16 @@ +${AnsiColor.BRIGHT_BLUE} + ██ ██ ████████ +░██ ░██ ██ ██ ██████ ██░░░░░░ +░██ ░██ ░░██ ██ ░██░░░██ █████ ██████░██ █████ ███████ ██████ █████ +░██████████ ░░███ ░██ ░██ ██░░░██░░██░░█░█████████ ██░░░██░░██░░░██ ██░░░░ ██░░░██ +░██░░░░░░██ ░██ ░██████ ░███████ ░██ ░ ░░░░░░░░██░███████ ░██ ░██░░█████ ░███████ +░██ ░██ ██ ░██░░░ ░██░░░░ ░██ ░██░██░░░░ ░██ ░██ ░░░░░██░██░░░░ +░██ ░██ ██ ░██ ░░██████░███ ████████ ░░██████ ███ ░██ ██████ ░░██████ +░░ ░░ ░░ ░░ ░░░░░░ ░░░ ░░░░░░░░ ░░░░░░ ░░░ ░░ ░░░░░░ ░░░░░░ + +${AnsiColor.BRIGHT_BLUE} +HyperSense Boot Version: ${project.version} +Spring Boot Version: ${spring-boot.version}${spring-boot.formatted-version} +氦闪官网: https://www.hypersense.tech/ +版权所属: 氦闪技术分享 +${AnsiColor.CYAN} \ No newline at end of file diff --git a/hypersense-access/src/main/resources/ip2region.xdb b/hypersense-access/src/main/resources/ip2region.xdb new file mode 100644 index 0000000000000000000000000000000000000000..31f96a1fb1695b14c86a73a0cc14fa6c600263c1 Binary files /dev/null and b/hypersense-access/src/main/resources/ip2region.xdb differ diff --git a/hypersense-access/src/main/resources/logback-spring.xml b/hypersense-access/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..a424b782f2ea26c283f1b0315dbfdad602ea84a4 --- /dev/null +++ b/hypersense-access/src/main/resources/logback-spring.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + DEBUG + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + + + ${LOG_HOME}/log.log + + + %d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%n + UTF-8 + + + + + ${LOG_HOME}/%d{yyyy-MM-dd}.%i.log + + 10MB + + 30 + + 1GB + + + + INFO + + + + + + + + + + + + + + + + + + diff --git a/hypersense-common/hypersense-common-bom/pom.xml b/hypersense-common/hypersense-common-bom/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0ec183741a24b492b9a9c07a6f8db11e95a2ac60 --- /dev/null +++ b/hypersense-common/hypersense-common-bom/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + tech.hypersense + hypersense-common-bom + ${revision} + pom + + + hypersense-common-bom common模块依赖项 + + + + 1.0.0 + + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-common-web + ${revision} + + + + tech.hypersense + hypersense-common-security + ${revision} + + + + tech.hypersense + hypersense-common-websocket + ${revision} + + + + tech.hypersense + hypersense-common-log + ${revision} + + + + tech.hypersense + hypersense-common-doc + ${revision} + + + + tech.hypersense + hypersense-common-mybatis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-core/pom.xml b/hypersense-common/hypersense-common-core/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..aeaf612da16b97affe4b6597bc64fcfa6aafa15d --- /dev/null +++ b/hypersense-common/hypersense-common-core/pom.xml @@ -0,0 +1,142 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-core + + hypersense-common-core 核心模块 + + + + + + org.springframework + spring-context-support + + + + + org.springframework + spring-web + + + + + org.springframework + spring-messaging + + + + + org.springframework + spring-aspects + + + + + com.github.ben-manes.caffeine + caffeine + + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + + + jakarta.servlet + jakarta.servlet-api + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-configuration-processor + + + + org.springframework.boot + spring-boot-properties-migrator + runtime + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.mysql + mysql-connector-j + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + + + + + cn.hutool + hutool-all + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + + org.projectlombok + lombok-mapstruct-binding + provided + + + + io.github.linpeilie + mapstruct-plus-spring-boot-starter + + + + + org.lionsoul + ip2region + + + + com.alibaba + easyexcel + + + + org.lionsoul + ip2region + + + + org.apache.velocity + velocity-engine-core + + + + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/annotation/DataPermission.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/annotation/DataPermission.java new file mode 100644 index 0000000000000000000000000000000000000000..751c0e11817f2d2e8dddca32f095935589683458 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/annotation/DataPermission.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.core.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 数据权限注解 + * @Version: 1.0 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface DataPermission { + + /** + * 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor} + */ + String deptAlias() default ""; + + String deptIdColumnName() default "dept_id"; + + String userAlias() default ""; + + String userIdColumnName() default "create_by"; + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/annotation/ValidField.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/annotation/ValidField.java new file mode 100644 index 0000000000000000000000000000000000000000..848bb10edf9b4d8c8efd2c31045482854fad47c4 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/annotation/ValidField.java @@ -0,0 +1,35 @@ +package tech.hypersense.common.core.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import tech.hypersense.common.core.validator.FieldValidator; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用于验证字段值是否合法的注解 + * @Version: 1.0 + */ +@Documented +@Constraint(validatedBy = FieldValidator.class) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidField { + + /** + * 验证失败时的错误信息。 + */ + String message() default "非法字段"; + + Class>[] groups() default {}; + + Class extends Payload>[] payload() default {}; + + /** + * 允许的合法值列表。 + */ + String[] allowedValues(); + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/config/CaffeineConfig.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/config/CaffeineConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..d6e4375d472eb879a960ec8764e16f06fa2de4d9 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/config/CaffeineConfig.java @@ -0,0 +1,36 @@ +package tech.hypersense.common.core.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; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: caffeine缓存配置 + * @Version: 1.0 + */ +@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/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/JwtClaimConstants.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/JwtClaimConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..a43a00e329fb6dd5aa3e9425e1b49caabbb5fcb4 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/JwtClaimConstants.java @@ -0,0 +1,29 @@ +package tech.hypersense.common.core.constant; +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: JWT Claims声明常量 JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。 +*@Version: 1.0 +*/ +public interface JwtClaimConstants { + + /** + * 用户ID + */ + String USER_ID = "userId"; + + /** + * 部门ID + */ + String DEPT_ID = "deptId"; + + /** + * 数据权限 + */ + String DATA_SCOPE = "dataScope"; + + /** + * 权限(角色Code)集合 + */ + String AUTHORITIES = "authorities"; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/RedisConstants.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/RedisConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..453cba3475ce6e9b53b6458ded480035e499a6f5 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/RedisConstants.java @@ -0,0 +1,60 @@ +package tech.hypersense.common.core.constant; +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: Redis 常量 +*@Version: 1.0 +*/ +public interface RedisConstants { + + + /** + * 限流相关键 + */ + interface RateLimiter { + String IP = "rate_limiter:ip:{}"; // IP限流(示例:rate_limiter:ip:192.168.1.1) + } + + /** + * 分布式锁相关键 + */ + interface Lock { + String RESUBMIT = "lock:resubmit:{}:{}"; // 防重复提交(示例:lock:resubmit:userIdentifier:requestIdentifier) + } + + /** + * 认证模块 + */ + interface Auth { + // 存储访问令牌对应的用户信息(accessToken -> OnlineUser) + String ACCESS_TOKEN_USER = "auth:token:access:{}"; + // 存储刷新令牌对应的用户信息(refreshToken -> OnlineUser) + String REFRESH_TOKEN_USER = "auth:token:refresh:{}"; + // 用户与访问令牌的映射(userId -> accessToken) + String USER_ACCESS_TOKEN = "auth:user:access:{}"; + // 用户与刷新令牌的映射(userId -> refreshToken + String USER_REFRESH_TOKEN = "auth:user:refresh:{}"; + // 黑名单 Token(用于退出登录或注销) + String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; + } + + /** + * 验证码模块 + */ + interface Captcha { + String IMAGE_CODE = "captcha:image:{}"; // 图形验证码 + String SMS_LOGIN_CODE = "captcha:sms_login:{}"; // 登录短信验证码 + String SMS_REGISTER_CODE = "captcha:sms_register:{}";// 注册短信验证码 + String MOBILE_CODE = "captcha:mobile:{}"; // 绑定、更换手机验证码 + String EMAIL_CODE = "captcha:email:{}"; // 邮箱验证码 + } + + /** + * 系统模块 + */ + interface System { + String CONFIG = "system:config"; // 系统配置 + String ROLE_PERMS = "system:role:perms"; // 系统角色和权限映射 + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SecurityConstants.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SecurityConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..57e2d46e9d86710be43b14457df1e4f7aad04bbe --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SecurityConstants.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.core.constant; +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: 安全模块常量 +*@Version: 1.0 +*/ +public interface SecurityConstants { + + /** + * 登录路径 + */ + String LOGIN_PATH = "/api/v1/auth/login"; + + /** + * JWT Token 前缀 + */ + String BEARER_TOKEN_PREFIX = "Bearer "; + + /** + * 角色前缀,用于区分 authorities 角色和权限, ROLE_* 角色 、没有前缀的是权限 + */ + String ROLE_PREFIX = "ROLE_"; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SystemConstants.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SystemConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..f287e9a940e0b880eae263bf621797a7b8b77d44 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SystemConstants.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.core.constant; +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: 系统常量 +*@Version: 1.0 +*/ +public interface SystemConstants { + + /** + * 根节点ID + */ + Long ROOT_NODE_ID = 0L; + + /** + * 系统默认密码 + */ + String DEFAULT_PASSWORD = "123456"; + + /** + * 超级管理员角色编码 + */ + String ROOT_ROLE_CODE = "ROOT"; + + + /** + * 系统配置 IP的QPS限流的KEY + */ + String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT"; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/handler/CommonMetaObjectHandler.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/handler/CommonMetaObjectHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..ba82c652dc548b4f3672b2103676ad7ac011b2d4 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/handler/CommonMetaObjectHandler.java @@ -0,0 +1,38 @@ +package tech.hypersense.common.core.domain.handler; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 定义公共Mybatis-PLUS 字段拦截,各个业务模块继承该公共类 + * @Version: 1.0 + */ +@Component +public class CommonMetaObjectHandler implements MetaObjectHandler { + + /** + * 新增填充创建时间 + * + * @param metaObject 元数据 + */ + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class); + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); + } + + /** + * 更新填充更新时间 + * + * @param metaObject 元数据 + */ + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/KValue.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/KValue.java new file mode 100644 index 0000000000000000000000000000000000000000..792f89d9380526baed828d9863df3683e2043c6a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/KValue.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.core.domain.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 键值对 + * @Version: 1.0 + */ +@Schema(description = "键值对") +@Data +@NoArgsConstructor +public class KValue { + public KValue(String key, String value) { + this.key = key; + this.value = value; + } + + @Schema(description = "选项的值") + private String key; + + @Schema(description = "选项的标签") + private String value; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/Option.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/Option.java new file mode 100644 index 0000000000000000000000000000000000000000..97b577cd230642fb982aec48078ee4c85741e92c --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/Option.java @@ -0,0 +1,53 @@ +package tech.hypersense.common.core.domain.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 下拉选项对象 + * @Version: 1.0 + */ +@Schema(description ="下拉选项对象") +@Data +@NoArgsConstructor +public class Option { + + public Option(T value, String label) { + this.value = value; + this.label = label; + } + + public Option(T value, String label, List> children) { + this.value = value; + this.label = label; + this.children= children; + } + + public Option(T value, String label, String tag) { + this.value = value; + this.label = label; + this.tag= tag; + } + + + @Schema(description="选项的值") + private T value; + + @Schema(description="选项的标签") + private String label; + + @Schema(description = "标签类型") + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String tag; + + @Schema(description="子选项列表") + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private List> children; + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseEntity.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..32cbc100d38523ce247612cf6d5353be135496cb --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseEntity.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.core.domain.model.base; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等 + * @Version: 1.0 + */ +@Data +public class BaseEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BasePageQuery.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BasePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..17c772137856f066cf41716b6547892a43e44e35 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BasePageQuery.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.core.domain.model.base; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 基础分页类 + * @Version: 1.0 + */ +@Data +@Schema +public class BasePageQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "页码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private int pageNum = 1; + + @Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private int pageSize = 10; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseVo.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseVo.java new file mode 100644 index 0000000000000000000000000000000000000000..a3493d8e7160ba59bf054bc7cecdbd34acf2b000 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseVo.java @@ -0,0 +1,21 @@ +package tech.hypersense.common.core.domain.model.base; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 视图层基本类 + * @Version: 1.0 + */ +@Data +@Schema +public class BaseVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/modules/system/dto/UserAuthInfo.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/modules/system/dto/UserAuthInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..3faea337e828615f842712e5614130c954f9614e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/modules/system/dto/UserAuthInfo.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.domain.model.modules.system.dto; + +import lombok.Data; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户认证信息 + * @Version: 1.0 + */ +@Data +public class UserAuthInfo { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户密码 + */ + private String password; + + /** + * 状态(1:启用;0:禁用) + */ + private Integer status; + + /** + * 用户所属的角色集合 + */ + private Set roles; + + /** + * 数据权限范围,用于控制用户可以访问的数据级别 + * + * + */ + private Integer dataScope; + +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/shared/codegen/entity/GenConfig.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/shared/codegen/entity/GenConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..4ee356e5e3e16311a47af15fc9ebd1dd86938618 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/shared/codegen/entity/GenConfig.java @@ -0,0 +1,53 @@ +package tech.hypersense.common.core.domain.model.shared.codegen.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置 + * @Version: 1.0 + */ +@TableName(value = "gen_config") +@Getter +@Setter +public class GenConfig extends BaseEntity { + + /** + * 表名 + */ + private String tableName; + + /** + * 包名 + */ + private String packageName; + + /** + * 模块名 + */ + private String moduleName; + + /** + * 实体类名 + */ + private String entityName; + + /** + * 业务名 + */ + private String businessName; + + /** + * 父菜单ID + */ + private Long parentMenuId; + + /** + * 作者 + */ + private String author; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/ExcelResult.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/ExcelResult.java new file mode 100644 index 0000000000000000000000000000000000000000..fae5186c45fa2058302e76abc87cb2a4e403c765 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/ExcelResult.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.core.domain.result; + +import lombok.Data; +import tech.hypersense.common.core.enums.ResultCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel导出响应结构体 + * @Version: 1.0 + */ +@Data +public class ExcelResult { + + /** + * 响应码,来确定是否导入成功 + */ + private String code; + + /** + * 有效条数 + */ + private Integer validCount; + + /** + * 无效条数 + */ + private Integer invalidCount; + + /** + * 错误提示信息 + */ + private List messageList; + + public ExcelResult() { + this.code = ResultCode.SUCCESS.getCode(); + this.validCount = 0; + this.invalidCount = 0; + this.messageList = new ArrayList<>(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/PageResult.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/PageResult.java new file mode 100644 index 0000000000000000000000000000000000000000..9142026a666d9660f2f30f827fab39a6dbad2ce6 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/PageResult.java @@ -0,0 +1,46 @@ +package tech.hypersense.common.core.domain.result; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.Data; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.Serializable; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 分页响应结构体 + * @Version: 1.0 + */ +@Data +public class PageResult implements Serializable { + + private String code; + + private Data data; + + private String msg; + + public static PageResult success(IPage page) { + PageResult result = new PageResult<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + + Data data = new Data<>(); + data.setList(page.getRecords()); + data.setTotal(page.getTotal()); + + result.setData(data); + result.setMsg(ResultCode.SUCCESS.getMsg()); + return result; + } + + @lombok.Data + public static class Data { + + private List list; + + private long total; + + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/Result.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/Result.java new file mode 100644 index 0000000000000000000000000000000000000000..250f49abf8b3724582d6469742415dd569874ec7 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/Result.java @@ -0,0 +1,76 @@ +package tech.hypersense.common.core.domain.result; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import tech.hypersense.common.core.domain.result.base.IResultCode; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应结构体 + * @Version: 1.0 + */ +@Data +public class Result implements Serializable { + + private String code; + + private T data; + + private String msg; + + public static Result success() { + return success(null); + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMsg(ResultCode.SUCCESS.getMsg()); + result.setData(data); + return result; + } + + public static Result failed() { + return result(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMsg(), null); + } + + public static Result failed(String msg) { + return result(ResultCode.SYSTEM_ERROR.getCode(), msg, null); + } + + public static Result judge(boolean status) { + if (status) { + return success(); + } else { + return failed(); + } + } + + public static Result failed(IResultCode resultCode) { + return result(resultCode.getCode(), resultCode.getMsg(), null); + } + + public static Result failed(IResultCode resultCode, String msg) { + return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null); + } + + private static Result result(IResultCode resultCode, T data) { + return result(resultCode.getCode(), resultCode.getMsg(), data); + } + + private static Result result(String code, String msg, T data) { + Result result = new Result<>(); + result.setCode(code); + result.setData(data); + result.setMsg(msg); + return result; + } + + public static boolean isSuccess(Result> result) { + return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode()); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/base/IResultCode.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/base/IResultCode.java new file mode 100644 index 0000000000000000000000000000000000000000..04adc1abbdb9b19dfcb6c21905914f2c120ca20b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/base/IResultCode.java @@ -0,0 +1,14 @@ +package tech.hypersense.common.core.domain.result.base; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应码接口 + * @Version: 1.0 + */ +public interface IResultCode { + + String getCode(); + + String getMsg(); +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/DataScopeEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/DataScopeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..aee45031d4af5d78425b967ef8925079d9f5c835 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/DataScopeEnum.java @@ -0,0 +1,31 @@ +package tech.hypersense.common.core.enums; + +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 数据权限枚举 + * @Version: 1.0 + */ +@Getter +public enum DataScopeEnum implements IBaseEnum { + + /** + * value 越小,数据权限范围越大 + */ + ALL(0, "所有数据"), + DEPT_AND_SUB(1, "部门及子部门数据"), + DEPT(2, "本部门数据"), + SELF(3, "本人数据"); + + private final Integer value; + + private final String label; + + DataScopeEnum(Integer value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/EnvEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/EnvEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..8b7da27dc5531c6cb2d02f343db4206f5de2389e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/EnvEnum.java @@ -0,0 +1,26 @@ +package tech.hypersense.common.core.enums; + +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 环境枚举 + * @Version: 1.0 + */ +@Getter +public enum EnvEnum implements IBaseEnum { + + DEV("dev", "开发环境"), + PROD("prod", "生产环境"); + + private final String value; + + private final String label; + + EnvEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/LogModuleEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/LogModuleEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..8202c198276e5768bb73900afbc82e338faaf2f4 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/LogModuleEnum.java @@ -0,0 +1,33 @@ +package tech.hypersense.common.core.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日志模块枚举 + * @Version: 1.0 + */ +@Schema(enumAsRef = true) +@Getter +public enum LogModuleEnum { + + EXCEPTION("异常"), + LOGIN("登录"), + USER("用户"), + DEPT("部门"), + ROLE("角色"), + MENU("菜单"), + DICT("字典"), + SETTING("系统配置"), + OTHER("其他"); + + @JsonValue + private final String moduleName; + + LogModuleEnum(String moduleName) { + this.moduleName = moduleName; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/RequestMethodEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/RequestMethodEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..e7391326da039d77a973d6a22a31bfd2ff0699c2 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/RequestMethodEnum.java @@ -0,0 +1,58 @@ +package tech.hypersense.common.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@Getter +@AllArgsConstructor +public enum RequestMethodEnum { + /** + * 搜寻 @AnonymousGetMapping + */ + GET("GET"), + + /** + * 搜寻 @AnonymousPostMapping + */ + POST("POST"), + + /** + * 搜寻 @AnonymousPutMapping + */ + PUT("PUT"), + + /** + * 搜寻 @AnonymousPatchMapping + */ + PATCH("PATCH"), + + /** + * 搜寻 @AnonymousDeleteMapping + */ + DELETE("DELETE"), + + /** + * 否则就是所有 Request 接口都放行 + */ + ALL("All"); + + /** + * Request 类型 + */ + private final String type; + + public static RequestMethodEnum find(String type) { + for (RequestMethodEnum value : RequestMethodEnum.values()) { + if (value.getType().equals(type)) { + return value; + } + } + return ALL; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/ResultCode.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/ResultCode.java new file mode 100644 index 0000000000000000000000000000000000000000..13f88154130b931f44d1932a605d473c8485261e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/ResultCode.java @@ -0,0 +1,290 @@ +package tech.hypersense.common.core.enums; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import tech.hypersense.common.core.domain.result.base.IResultCode; + +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应码枚举:参考阿里巴巴开发手册响应码规范 + * @Version: 1.0 + */ +@AllArgsConstructor +@NoArgsConstructor +public enum ResultCode implements IResultCode, Serializable { + + SUCCESS("00000", "一切ok"), + + /** 一级宏观错误码 */ + USER_ERROR("A0001", "用户端错误"), + + /** 二级宏观错误码 */ + USER_REGISTRATION_ERROR("A0100", "用户注册错误"), + USER_NOT_AGREE_PRIVACY_AGREEMENT("A0101", "用户未同意隐私协议"), + REGISTRATION_COUNTRY_OR_REGION_RESTRICTED("A0102", "注册国家或地区受限"), + + USERNAME_VERIFICATION_FAILED("A0110", "用户名校验失败"), + USERNAME_ALREADY_EXISTS("A0111", "用户名已存在"), + USERNAME_CONTAINS_SENSITIVE_WORDS("A0112", "用户名包含敏感词"), + USERNAME_CONTAINS_SPECIAL_CHARACTERS("A0113", "用户名包含特殊字符"), + + PASSWORD_VERIFICATION_FAILED("A0120", "密码校验失败"), + PASSWORD_LENGTH_NOT_ENOUGH("A0121", "密码长度不够"), + PASSWORD_STRENGTH_NOT_ENOUGH("A0122", "密码强度不够"), + + VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"), + SMS_VERIFICATION_CODE_INPUT_ERROR("A0131", "短信校验码输入错误"), + EMAIL_VERIFICATION_CODE_INPUT_ERROR("A0132", "邮件校验码输入错误"), + VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"), + + USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"), + USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证件类型未选择"), + MAINLAND_ID_NUMBER_VERIFICATION_ILLEGAL("A0142", "大陆身份证编号校验非法"), + + USER_BASIC_INFORMATION_VERIFICATION_FAILED("A0150", "用户基本信息校验失败"), + PHONE_FORMAT_VERIFICATION_FAILED("A0151", "手机格式校验失败"), + ADDRESS_FORMAT_VERIFICATION_FAILED("A0152", "地址格式校验失败"), + EMAIL_FORMAT_VERIFICATION_FAILED("A0153", "邮箱格式校验失败"), + + /** 二级宏观错误码 */ + USER_LOGIN_EXCEPTION("A0200", "用户登录异常"), + USER_ACCOUNT_FROZEN("A0201", "用户账户被冻结"), + USER_ACCOUNT_ABOLISHED("A0202", "用户账户已作废"), + + USER_PASSWORD_ERROR("A0210", "用户名或密码错误"), + USER_INPUT_PASSWORD_ERROR_LIMIT_EXCEEDED("A0211", "用户输入密码错误次数超限"), + + USER_IDENTITY_VERIFICATION_FAILED("A0220", "用户身份校验失败"), + USER_FINGERPRINT_RECOGNITION_FAILED("A0221", "用户指纹识别失败"), + USER_FACE_RECOGNITION_FAILED("A0222", "用户面容识别失败"), + USER_NOT_AUTHORIZED_THIRD_PARTY_LOGIN("A0223", "用户未获得第三方登录授权"), + + ACCESS_TOKEN_INVALID("A0230", "访问令牌无效或已过期"), + REFRESH_TOKEN_INVALID("A0231", "刷新令牌无效或已过期"), + + // 验证码错误 + USER_VERIFICATION_CODE_ERROR("A0240", "验证码错误"), + USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"), + USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"), + + /** 二级宏观错误码 */ + ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"), + ACCESS_UNAUTHORIZED("A0301", "访问未授权"), + AUTHORIZATION_IN_PROGRESS("A0302", "正在授权中"), + USER_AUTHORIZATION_APPLICATION_REJECTED("A0303", "用户授权申请被拒绝"), + + ACCESS_OBJECT_PRIVACY_SETTINGS_BLOCKED("A0310", "因访问对象隐私设置被拦截"), + AUTHORIZATION_EXPIRED("A0311", "授权已过期"), + NO_PERMISSION_TO_USE_API("A0312", "无权限使用 API"), + + USER_ACCESS_BLOCKED("A0320", "用户访问被拦截"), + BLACKLISTED_USER("A0321", "黑名单用户"), + ACCOUNT_FROZEN("A0322", "账号被冻结"), + ILLEGAL_IP_ADDRESS("A0323", "非法 IP 地址"), + GATEWAY_ACCESS_RESTRICTED("A0324", "网关访问受限"), + REGION_BLACKLIST("A0325", "地域黑名单"), + + SERVICE_ARREARS("A0330", "服务已欠费"), + + USER_SIGNATURE_EXCEPTION("A0340", "用户签名异常"), + RSA_SIGNATURE_ERROR("A0341", "RSA 签名错误"), + + /** 二级宏观错误码 */ + USER_REQUEST_PARAMETER_ERROR("A0400", "用户请求参数错误"), + CONTAINS_ILLEGAL_MALICIOUS_REDIRECT_LINK("A0401", "包含非法恶意跳转链接"), + INVALID_USER_INPUT("A0402", "无效的用户输入"), + + REQUEST_REQUIRED_PARAMETER_IS_EMPTY("A0410", "请求必填参数为空"), + + REQUEST_PARAMETER_VALUE_EXCEEDS_ALLOWED_RANGE("A0420", "请求参数值超出允许的范围"), + PARAMETER_FORMAT_MISMATCH("A0421", "参数格式不匹配"), + + USER_INPUT_CONTENT_ILLEGAL("A0430", "用户输入内容非法"), + CONTAINS_PROHIBITED_SENSITIVE_WORDS("A0431", "包含违禁敏感词"), + + USER_OPERATION_EXCEPTION("A0440", "用户操作异常"), + + /** 二级宏观错误码 */ + USER_REQUEST_SERVICE_EXCEPTION("A0500", "用户请求服务异常"), + REQUEST_LIMIT_EXCEEDED("A0501", "请求次数超出限制"), + REQUEST_CONCURRENCY_LIMIT_EXCEEDED("A0502", "请求并发数超出限制"), + USER_OPERATION_PLEASE_WAIT("A0503", "用户操作请等待"), + WEBSOCKET_CONNECTION_EXCEPTION("A0504", "WebSocket 连接异常"), + WEBSOCKET_CONNECTION_DISCONNECTED("A0505", "WebSocket 连接断开"), + USER_DUPLICATE_REQUEST("A0506", "请求过于频繁,请稍后再试。"), + + /** 二级宏观错误码 */ + USER_RESOURCE_EXCEPTION("A0600", "用户资源异常"), + ACCOUNT_BALANCE_INSUFFICIENT("A0601", "账户余额不足"), + USER_DISK_SPACE_INSUFFICIENT("A0602", "用户磁盘空间不足"), + USER_MEMORY_SPACE_INSUFFICIENT("A0603", "用户内存空间不足"), + USER_OSS_CAPACITY_INSUFFICIENT("A0604", "用户 OSS 容量不足"), + USER_QUOTA_EXHAUSTED("A0605", "用户配额已用光"), + USER_RESOURCE_NOT_FOUND("A0606", "用户资源不存在"), + + /** 二级宏观错误码 */ + UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"), + UPLOAD_FILE_TYPE_MISMATCH("A0701", "上传文件类型不匹配"), + UPLOAD_FILE_TOO_LARGE("A0702", "上传文件太大"), + UPLOAD_IMAGE_TOO_LARGE("A0703", "上传图片太大"), + UPLOAD_VIDEO_TOO_LARGE("A0704", "上传视频太大"), + UPLOAD_COMPRESSED_FILE_TOO_LARGE("A0705", "上传压缩文件太大"), + + DELETE_FILE_EXCEPTION("A0710", "删除文件异常"), + + /** 二级宏观错误码 */ + USER_CURRENT_VERSION_EXCEPTION("A0800", "用户当前版本异常"), + USER_INSTALLED_VERSION_NOT_MATCH_SYSTEM("A0801", "用户安装版本与系统不匹配"), + USER_INSTALLED_VERSION_TOO_LOW("A0802", "用户安装版本过低"), + USER_INSTALLED_VERSION_TOO_HIGH("A0803", "用户安装版本过高"), + USER_INSTALLED_VERSION_EXPIRED("A0804", "用户安装版本已过期"), + USER_API_REQUEST_VERSION_NOT_MATCH("A0805", "用户 API 请求版本不匹配"), + USER_API_REQUEST_VERSION_TOO_HIGH("A0806", "用户 API 请求版本过高"), + USER_API_REQUEST_VERSION_TOO_LOW("A0807", "用户 API 请求版本过低"), + + /** 二级宏观错误码 */ + USER_PRIVACY_NOT_AUTHORIZED("A0900", "用户隐私未授权"), + USER_PRIVACY_NOT_SIGNED("A0901", "用户隐私未签署"), + USER_CAMERA_NOT_AUTHORIZED("A0903", "用户相机未授权"), + USER_PHOTO_LIBRARY_NOT_AUTHORIZED("A0904", "用户图片库未授权"), + USER_FILE_NOT_AUTHORIZED("A0905", "用户文件未授权"), + USER_LOCATION_INFORMATION_NOT_AUTHORIZED("A0906", "用户位置信息未授权"), + USER_CONTACTS_NOT_AUTHORIZED("A0907", "用户通讯录未授权"), + + /** 二级宏观错误码 */ + USER_DEVICE_EXCEPTION("A1000", "用户设备异常"), + USER_CAMERA_EXCEPTION("A1001", "用户相机异常"), + USER_MICROPHONE_EXCEPTION("A1002", "用户麦克风异常"), + USER_EARPIECE_EXCEPTION("A1003", "用户听筒异常"), + USER_SPEAKER_EXCEPTION("A1004", "用户扬声器异常"), + USER_GPS_POSITIONING_EXCEPTION("A1005", "用户 GPS 定位异常"), + + /** 一级宏观错误码 */ + SYSTEM_ERROR("B0001", "系统执行出错"), + + /** 二级宏观错误码 */ + SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"), + + /** 二级宏观错误码 */ + SYSTEM_DISASTER_RECOVERY_FUNCTION_TRIGGERED("B0200", "系统容灾功能被触发"), + + SYSTEM_RATE_LIMITING("B0210", "系统限流"), + + SYSTEM_FUNCTION_DEGRADATION("B0220", "系统功能降级"), + + /** 二级宏观错误码 */ + SYSTEM_RESOURCE_EXCEPTION("B0300", "系统资源异常"), + SYSTEM_RESOURCE_EXHAUSTED("B0310", "系统资源耗尽"), + SYSTEM_DISK_SPACE_EXHAUSTED("B0311", "系统磁盘空间耗尽"), + SYSTEM_MEMORY_EXHAUSTED("B0312", "系统内存耗尽"), + FILE_HANDLE_EXHAUSTED("B0313", "文件句柄耗尽"), + SYSTEM_CONNECTION_POOL_EXHAUSTED("B0314", "系统连接池耗尽"), + SYSTEM_THREAD_POOL_EXHAUSTED("B0315", "系统线程池耗尽"), + + SYSTEM_RESOURCE_ACCESS_EXCEPTION("B0320", "系统资源访问异常"), + SYSTEM_READ_DISK_FILE_FAILED("B0321", "系统读取磁盘文件失败"), + + + /** 一级宏观错误码 */ + THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"), + + /** 二级宏观错误码 */ + MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"), + + RPC_SERVICE_ERROR("C0110", "RPC 服务出错"), + RPC_SERVICE_NOT_FOUND("C0111", "RPC 服务未找到"), + RPC_SERVICE_NOT_REGISTERED("C0112", "RPC 服务未注册"), + INTERFACE_NOT_EXIST("C0113", "接口不存在"), + + MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"), + MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"), + MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"), + MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"), + MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"), + + CACHE_SERVICE_ERROR("C0130", "缓存服务出错"), + KEY_LENGTH_EXCEEDS_LIMIT("C0131", "key 长度超过限制"), + VALUE_LENGTH_EXCEEDS_LIMIT("C0132", "value 长度超过限制"), + STORAGE_CAPACITY_FULL("C0133", "存储容量已满"), + UNSUPPORTED_DATA_FORMAT("C0134", "不支持的数据格式"), + + CONFIGURATION_SERVICE_ERROR("C0140", "配置服务出错"), + + NETWORK_RESOURCE_SERVICE_ERROR("C0150", "网络资源服务出错"), + VPN_SERVICE_ERROR("C0151", "VPN 服务出错"), + CDN_SERVICE_ERROR("C0152", "CDN 服务出错"), + DOMAIN_NAME_RESOLUTION_SERVICE_ERROR("C0153", "域名解析服务出错"), + GATEWAY_SERVICE_ERROR("C0154", "网关服务出错"), + + /** 二级宏观错误码 */ + THIRD_PARTY_SYSTEM_EXECUTION_TIMEOUT("C0200", "第三方系统执行超时"), + + RPC_EXECUTION_TIMEOUT("C0210", "RPC 执行超时"), + + MESSAGE_DELIVERY_TIMEOUT("C0220", "消息投递超时"), + + CACHE_SERVICE_TIMEOUT("C0230", "缓存服务超时"), + + CONFIGURATION_SERVICE_TIMEOUT("C0240", "配置服务超时"), + + DATABASE_SERVICE_TIMEOUT("C0250", "数据库服务超时"), + + /** 二级宏观错误码 */ + DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"), + + TABLE_NOT_EXIST("C0311", "表不存在"), + COLUMN_NOT_EXIST("C0312", "列不存在"), + + MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"), + + DATABASE_DEADLOCK("C0331", "数据库死锁"), + + PRIMARY_KEY_CONFLICT("C0341", "主键冲突"), + + /** 二级宏观错误码 */ + THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"), + THIRD_PARTY_SYSTEM_RATE_LIMITING("C0401", "第三方系统限流"), + THIRD_PARTY_FUNCTION_DEGRADATION("C0402", "第三方功能降级"), + + /** 二级宏观错误码 */ + NOTIFICATION_SERVICE_ERROR("C0500", "通知服务出错"), + SMS_REMINDER_SERVICE_FAILED("C0501", "短信提醒服务失败"), + VOICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"), + EMAIL_REMINDER_SERVICE_FAILED("C0503", "邮件提醒服务失败"); + + + @Override + public String getCode() { + return code; + } + + @Override + public String getMsg() { + return msg; + } + + private String code; + + private String msg; + + @Override + public String toString() { + return "{" + + "\"code\":\"" + code + '\"' + + ", \"msg\":\"" + msg + '\"' + + '}'; + } + + + public static ResultCode getValue(String code) { + for (ResultCode value : values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return SYSTEM_ERROR; // 默认系统执行错误 + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/StatusEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/StatusEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..0c6394a668cade200f7e4384bf982219d0e6aa83 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/StatusEnum.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.core.enums; + +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 状态枚举 + * @Version: 1.0 + */ +@Getter +public enum StatusEnum implements IBaseEnum { + + ENABLE(1, "启用"), + DISABLE (0, "禁用"); + + private final Integer value; + + + private final String label; + + StatusEnum(Integer value, String label) { + this.value = value; + this.label = label; + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/base/IBaseEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/base/IBaseEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..b647205c0607d4276629214431504f4d529baef2 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/base/IBaseEnum.java @@ -0,0 +1,85 @@ +package tech.hypersense.common.core.enums.base; + +import cn.hutool.core.util.ObjectUtil; + +import java.util.EnumSet; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 枚举通用接口 + * @Version: 1.0 + */ +public interface IBaseEnum{ + + T getValue(); + + String getLabel(); + + /** + * 根据值获取枚举 + * + * @param value + * @param clazz + * @param 枚举 + * @return + */ + static & IBaseEnum> E getEnumByValue(Object value, Class clazz) { + Objects.requireNonNull(value); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getValue(), value)) + .findFirst() + .orElse(null); + return matchEnum; + } + + /** + * 根据文本标签获取值 + * + * @param value + * @param clazz + * @param + * @return + */ + static & IBaseEnum> String getLabelByValue(Object value, Class clazz) { + Objects.requireNonNull(value); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getValue(), value)) + .findFirst() + .orElse(null); + + String label = null; + if (matchEnum != null) { + label = matchEnum.getLabel(); + } + return label; + } + + + /** + * 根据文本标签获取值 + * + * @param label + * @param clazz + * @param + * @return + */ + static & IBaseEnum> Object getValueByLabel(String label, Class clazz) { + Objects.requireNonNull(label); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + String finalLabel = label; + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getLabel(), finalLabel)) + .findFirst() + .orElse(null); + + Object value = null; + if (matchEnum != null) { + value = matchEnum.getValue(); + } + return value; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/common/log/OperateLogEvent.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/common/log/OperateLogEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..fcc462cb6968402df3f3f79e9acc0c27267743d6 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/common/log/OperateLogEvent.java @@ -0,0 +1,101 @@ +package tech.hypersense.common.core.event.common.log; + +import lombok.Data; +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 操作日志事件 + * @Version: 1.0 + */ +@Data +public class OperateLogEvent implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 日志模块 + */ + private LogModuleEnum module; + + /** + * 请求方式 + */ + private String requestMethod; + + /** + * 请求参数 + */ + private String requestParams; + + /** + * 响应参数 + */ + private String responseContent; + + /** + * 日志内容 + */ + private String content; + + /** + * 请求路径 + */ + private String requestUri; + + /** + * IP 地址 + */ + private String ip; + + /** + * 省份 + */ + private String province; + + /** + * 城市 + */ + private String city; + + /** + * 浏览器 + */ + private String browser; + + /** + * 浏览器版本 + */ + private String browserVersion; + + /** + * 终端系统 + */ + private String os; + + /** + * 执行时间(毫秒) + */ + private Long executionTime; + + /** + * 创建人ID + */ + private Long createBy; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/moudules/system/UserConnectionEvent.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/moudules/system/UserConnectionEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..874bc14479ac668fc6ae6dbb4044b28695108773 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/moudules/system/UserConnectionEvent.java @@ -0,0 +1,37 @@ +package tech.hypersense.common.core.event.moudules.system; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户连接事件 + * @Version: 1.0 + */ +@Getter +public class UserConnectionEvent extends ApplicationEvent { + + /** + * 用户名 + */ + private final String username; + + /** + * 是否连接 + */ + private final boolean connected; + + /** + * 用户连接事件 + * + * @param source 事件源 + * @param username 用户名 + * @param connected 是否连接 + */ + public UserConnectionEvent(Object source, String username, boolean connected) { + super(source); + this.username = username; + this.connected = connected; + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/BusinessException.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/BusinessException.java new file mode 100644 index 0000000000000000000000000000000000000000..511c2688604c5466607d9eff7851e83f332c9ed2 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/BusinessException.java @@ -0,0 +1,46 @@ +package tech.hypersense.common.core.exception; + +import lombok.Getter; +import org.slf4j.helpers.MessageFormatter; +import tech.hypersense.common.core.domain.result.base.IResultCode; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 自定义业务异常 + * @Version: 1.0 + */ +@Getter +public class BusinessException extends RuntimeException { + + public IResultCode resultCode; + + public BusinessException(IResultCode errorCode) { + super(errorCode.getMsg()); + this.resultCode = errorCode; + } + + + public BusinessException(IResultCode errorCode,String message) { + super(message); + this.resultCode = errorCode; + } + + + public BusinessException(String message, Throwable cause) { + super(message, cause); + } + + public BusinessException(Throwable cause) { + super(cause); + } + + public BusinessException(String message, Object... args) { + super(formatMessage(message, args)); + } + + private static String formatMessage(String message, Object... args) { + return MessageFormatter.arrayFormat(message, args).getMessage(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/handler/GlobalExceptionHandler.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..4fdf631e4255fefeebb1cbe3868332601f885b78 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,267 @@ +package tech.hypersense.common.core.exception.handler; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.ServletException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; + +import java.sql.SQLSyntaxErrorException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + + /** + * 处理绑定异常 + * + * 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(BindException e) { + log.error("BindException:{}", e.getMessage()); + String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg); + } + + /** + * 处理 @RequestParam 参数校验异常 + * + * 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过), + * 会捕获到 ConstraintViolationException 异常。 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ConstraintViolationException e) { + log.error("ConstraintViolationException:{}", e.getMessage()); + String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理方法参数校验异常 + * + * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, + * 会抛出 MethodArgumentNotValidException 异常。 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException:{}", e.getMessage()); + String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理接口不存在的异常 + * + * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result processException(NoHandlerFoundException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.INTERFACE_NOT_EXIST); + } + + /** + * 处理缺少请求参数的异常 + * + * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); + } + + /** + * 处理方法参数类型不匹配的异常 + * + * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。 +*@Version: 1.0 +*/ +public interface JwtClaimConstants { + + /** + * 用户ID + */ + String USER_ID = "userId"; + + /** + * 部门ID + */ + String DEPT_ID = "deptId"; + + /** + * 数据权限 + */ + String DATA_SCOPE = "dataScope"; + + /** + * 权限(角色Code)集合 + */ + String AUTHORITIES = "authorities"; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/RedisConstants.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/RedisConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..453cba3475ce6e9b53b6458ded480035e499a6f5 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/RedisConstants.java @@ -0,0 +1,60 @@ +package tech.hypersense.common.core.constant; +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: Redis 常量 +*@Version: 1.0 +*/ +public interface RedisConstants { + + + /** + * 限流相关键 + */ + interface RateLimiter { + String IP = "rate_limiter:ip:{}"; // IP限流(示例:rate_limiter:ip:192.168.1.1) + } + + /** + * 分布式锁相关键 + */ + interface Lock { + String RESUBMIT = "lock:resubmit:{}:{}"; // 防重复提交(示例:lock:resubmit:userIdentifier:requestIdentifier) + } + + /** + * 认证模块 + */ + interface Auth { + // 存储访问令牌对应的用户信息(accessToken -> OnlineUser) + String ACCESS_TOKEN_USER = "auth:token:access:{}"; + // 存储刷新令牌对应的用户信息(refreshToken -> OnlineUser) + String REFRESH_TOKEN_USER = "auth:token:refresh:{}"; + // 用户与访问令牌的映射(userId -> accessToken) + String USER_ACCESS_TOKEN = "auth:user:access:{}"; + // 用户与刷新令牌的映射(userId -> refreshToken + String USER_REFRESH_TOKEN = "auth:user:refresh:{}"; + // 黑名单 Token(用于退出登录或注销) + String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; + } + + /** + * 验证码模块 + */ + interface Captcha { + String IMAGE_CODE = "captcha:image:{}"; // 图形验证码 + String SMS_LOGIN_CODE = "captcha:sms_login:{}"; // 登录短信验证码 + String SMS_REGISTER_CODE = "captcha:sms_register:{}";// 注册短信验证码 + String MOBILE_CODE = "captcha:mobile:{}"; // 绑定、更换手机验证码 + String EMAIL_CODE = "captcha:email:{}"; // 邮箱验证码 + } + + /** + * 系统模块 + */ + interface System { + String CONFIG = "system:config"; // 系统配置 + String ROLE_PERMS = "system:role:perms"; // 系统角色和权限映射 + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SecurityConstants.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SecurityConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..57e2d46e9d86710be43b14457df1e4f7aad04bbe --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SecurityConstants.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.core.constant; +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: 安全模块常量 +*@Version: 1.0 +*/ +public interface SecurityConstants { + + /** + * 登录路径 + */ + String LOGIN_PATH = "/api/v1/auth/login"; + + /** + * JWT Token 前缀 + */ + String BEARER_TOKEN_PREFIX = "Bearer "; + + /** + * 角色前缀,用于区分 authorities 角色和权限, ROLE_* 角色 、没有前缀的是权限 + */ + String ROLE_PREFIX = "ROLE_"; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SystemConstants.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SystemConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..f287e9a940e0b880eae263bf621797a7b8b77d44 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/constant/SystemConstants.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.core.constant; +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: 系统常量 +*@Version: 1.0 +*/ +public interface SystemConstants { + + /** + * 根节点ID + */ + Long ROOT_NODE_ID = 0L; + + /** + * 系统默认密码 + */ + String DEFAULT_PASSWORD = "123456"; + + /** + * 超级管理员角色编码 + */ + String ROOT_ROLE_CODE = "ROOT"; + + + /** + * 系统配置 IP的QPS限流的KEY + */ + String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT"; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/handler/CommonMetaObjectHandler.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/handler/CommonMetaObjectHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..ba82c652dc548b4f3672b2103676ad7ac011b2d4 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/handler/CommonMetaObjectHandler.java @@ -0,0 +1,38 @@ +package tech.hypersense.common.core.domain.handler; + +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 定义公共Mybatis-PLUS 字段拦截,各个业务模块继承该公共类 + * @Version: 1.0 + */ +@Component +public class CommonMetaObjectHandler implements MetaObjectHandler { + + /** + * 新增填充创建时间 + * + * @param metaObject 元数据 + */ + @Override + public void insertFill(MetaObject metaObject) { + this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class); + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); + } + + /** + * 更新填充更新时间 + * + * @param metaObject 元数据 + */ + @Override + public void updateFill(MetaObject metaObject) { + this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/KValue.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/KValue.java new file mode 100644 index 0000000000000000000000000000000000000000..792f89d9380526baed828d9863df3683e2043c6a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/KValue.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.core.domain.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 键值对 + * @Version: 1.0 + */ +@Schema(description = "键值对") +@Data +@NoArgsConstructor +public class KValue { + public KValue(String key, String value) { + this.key = key; + this.value = value; + } + + @Schema(description = "选项的值") + private String key; + + @Schema(description = "选项的标签") + private String value; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/Option.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/Option.java new file mode 100644 index 0000000000000000000000000000000000000000..97b577cd230642fb982aec48078ee4c85741e92c --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/Option.java @@ -0,0 +1,53 @@ +package tech.hypersense.common.core.domain.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 下拉选项对象 + * @Version: 1.0 + */ +@Schema(description ="下拉选项对象") +@Data +@NoArgsConstructor +public class Option { + + public Option(T value, String label) { + this.value = value; + this.label = label; + } + + public Option(T value, String label, List> children) { + this.value = value; + this.label = label; + this.children= children; + } + + public Option(T value, String label, String tag) { + this.value = value; + this.label = label; + this.tag= tag; + } + + + @Schema(description="选项的值") + private T value; + + @Schema(description="选项的标签") + private String label; + + @Schema(description = "标签类型") + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private String tag; + + @Schema(description="子选项列表") + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + private List> children; + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseEntity.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..32cbc100d38523ce247612cf6d5353be135496cb --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseEntity.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.core.domain.model.base; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等 + * @Version: 1.0 + */ +@Data +public class BaseEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + @JsonInclude(value = JsonInclude.Include.NON_NULL) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BasePageQuery.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BasePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..17c772137856f066cf41716b6547892a43e44e35 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BasePageQuery.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.core.domain.model.base; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 基础分页类 + * @Version: 1.0 + */ +@Data +@Schema +public class BasePageQuery implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "页码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private int pageNum = 1; + + @Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private int pageSize = 10; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseVo.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseVo.java new file mode 100644 index 0000000000000000000000000000000000000000..a3493d8e7160ba59bf054bc7cecdbd34acf2b000 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/base/BaseVo.java @@ -0,0 +1,21 @@ +package tech.hypersense.common.core.domain.model.base; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 视图层基本类 + * @Version: 1.0 + */ +@Data +@Schema +public class BaseVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/modules/system/dto/UserAuthInfo.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/modules/system/dto/UserAuthInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..3faea337e828615f842712e5614130c954f9614e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/modules/system/dto/UserAuthInfo.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.domain.model.modules.system.dto; + +import lombok.Data; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户认证信息 + * @Version: 1.0 + */ +@Data +public class UserAuthInfo { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户密码 + */ + private String password; + + /** + * 状态(1:启用;0:禁用) + */ + private Integer status; + + /** + * 用户所属的角色集合 + */ + private Set roles; + + /** + * 数据权限范围,用于控制用户可以访问的数据级别 + * + * + */ + private Integer dataScope; + +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/shared/codegen/entity/GenConfig.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/shared/codegen/entity/GenConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..4ee356e5e3e16311a47af15fc9ebd1dd86938618 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/model/shared/codegen/entity/GenConfig.java @@ -0,0 +1,53 @@ +package tech.hypersense.common.core.domain.model.shared.codegen.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置 + * @Version: 1.0 + */ +@TableName(value = "gen_config") +@Getter +@Setter +public class GenConfig extends BaseEntity { + + /** + * 表名 + */ + private String tableName; + + /** + * 包名 + */ + private String packageName; + + /** + * 模块名 + */ + private String moduleName; + + /** + * 实体类名 + */ + private String entityName; + + /** + * 业务名 + */ + private String businessName; + + /** + * 父菜单ID + */ + private Long parentMenuId; + + /** + * 作者 + */ + private String author; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/ExcelResult.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/ExcelResult.java new file mode 100644 index 0000000000000000000000000000000000000000..fae5186c45fa2058302e76abc87cb2a4e403c765 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/ExcelResult.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.core.domain.result; + +import lombok.Data; +import tech.hypersense.common.core.enums.ResultCode; + +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel导出响应结构体 + * @Version: 1.0 + */ +@Data +public class ExcelResult { + + /** + * 响应码,来确定是否导入成功 + */ + private String code; + + /** + * 有效条数 + */ + private Integer validCount; + + /** + * 无效条数 + */ + private Integer invalidCount; + + /** + * 错误提示信息 + */ + private List messageList; + + public ExcelResult() { + this.code = ResultCode.SUCCESS.getCode(); + this.validCount = 0; + this.invalidCount = 0; + this.messageList = new ArrayList<>(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/PageResult.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/PageResult.java new file mode 100644 index 0000000000000000000000000000000000000000..9142026a666d9660f2f30f827fab39a6dbad2ce6 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/PageResult.java @@ -0,0 +1,46 @@ +package tech.hypersense.common.core.domain.result; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.Data; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.Serializable; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 分页响应结构体 + * @Version: 1.0 + */ +@Data +public class PageResult implements Serializable { + + private String code; + + private Data data; + + private String msg; + + public static PageResult success(IPage page) { + PageResult result = new PageResult<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + + Data data = new Data<>(); + data.setList(page.getRecords()); + data.setTotal(page.getTotal()); + + result.setData(data); + result.setMsg(ResultCode.SUCCESS.getMsg()); + return result; + } + + @lombok.Data + public static class Data { + + private List list; + + private long total; + + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/Result.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/Result.java new file mode 100644 index 0000000000000000000000000000000000000000..250f49abf8b3724582d6469742415dd569874ec7 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/Result.java @@ -0,0 +1,76 @@ +package tech.hypersense.common.core.domain.result; + +import cn.hutool.core.util.StrUtil; +import lombok.Data; +import tech.hypersense.common.core.domain.result.base.IResultCode; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应结构体 + * @Version: 1.0 + */ +@Data +public class Result implements Serializable { + + private String code; + + private T data; + + private String msg; + + public static Result success() { + return success(null); + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMsg(ResultCode.SUCCESS.getMsg()); + result.setData(data); + return result; + } + + public static Result failed() { + return result(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMsg(), null); + } + + public static Result failed(String msg) { + return result(ResultCode.SYSTEM_ERROR.getCode(), msg, null); + } + + public static Result judge(boolean status) { + if (status) { + return success(); + } else { + return failed(); + } + } + + public static Result failed(IResultCode resultCode) { + return result(resultCode.getCode(), resultCode.getMsg(), null); + } + + public static Result failed(IResultCode resultCode, String msg) { + return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null); + } + + private static Result result(IResultCode resultCode, T data) { + return result(resultCode.getCode(), resultCode.getMsg(), data); + } + + private static Result result(String code, String msg, T data) { + Result result = new Result<>(); + result.setCode(code); + result.setData(data); + result.setMsg(msg); + return result; + } + + public static boolean isSuccess(Result> result) { + return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode()); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/base/IResultCode.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/base/IResultCode.java new file mode 100644 index 0000000000000000000000000000000000000000..04adc1abbdb9b19dfcb6c21905914f2c120ca20b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/domain/result/base/IResultCode.java @@ -0,0 +1,14 @@ +package tech.hypersense.common.core.domain.result.base; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应码接口 + * @Version: 1.0 + */ +public interface IResultCode { + + String getCode(); + + String getMsg(); +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/DataScopeEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/DataScopeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..aee45031d4af5d78425b967ef8925079d9f5c835 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/DataScopeEnum.java @@ -0,0 +1,31 @@ +package tech.hypersense.common.core.enums; + +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 数据权限枚举 + * @Version: 1.0 + */ +@Getter +public enum DataScopeEnum implements IBaseEnum { + + /** + * value 越小,数据权限范围越大 + */ + ALL(0, "所有数据"), + DEPT_AND_SUB(1, "部门及子部门数据"), + DEPT(2, "本部门数据"), + SELF(3, "本人数据"); + + private final Integer value; + + private final String label; + + DataScopeEnum(Integer value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/EnvEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/EnvEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..8b7da27dc5531c6cb2d02f343db4206f5de2389e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/EnvEnum.java @@ -0,0 +1,26 @@ +package tech.hypersense.common.core.enums; + +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 环境枚举 + * @Version: 1.0 + */ +@Getter +public enum EnvEnum implements IBaseEnum { + + DEV("dev", "开发环境"), + PROD("prod", "生产环境"); + + private final String value; + + private final String label; + + EnvEnum(String value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/LogModuleEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/LogModuleEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..8202c198276e5768bb73900afbc82e338faaf2f4 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/LogModuleEnum.java @@ -0,0 +1,33 @@ +package tech.hypersense.common.core.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日志模块枚举 + * @Version: 1.0 + */ +@Schema(enumAsRef = true) +@Getter +public enum LogModuleEnum { + + EXCEPTION("异常"), + LOGIN("登录"), + USER("用户"), + DEPT("部门"), + ROLE("角色"), + MENU("菜单"), + DICT("字典"), + SETTING("系统配置"), + OTHER("其他"); + + @JsonValue + private final String moduleName; + + LogModuleEnum(String moduleName) { + this.moduleName = moduleName; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/RequestMethodEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/RequestMethodEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..e7391326da039d77a973d6a22a31bfd2ff0699c2 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/RequestMethodEnum.java @@ -0,0 +1,58 @@ +package tech.hypersense.common.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@Getter +@AllArgsConstructor +public enum RequestMethodEnum { + /** + * 搜寻 @AnonymousGetMapping + */ + GET("GET"), + + /** + * 搜寻 @AnonymousPostMapping + */ + POST("POST"), + + /** + * 搜寻 @AnonymousPutMapping + */ + PUT("PUT"), + + /** + * 搜寻 @AnonymousPatchMapping + */ + PATCH("PATCH"), + + /** + * 搜寻 @AnonymousDeleteMapping + */ + DELETE("DELETE"), + + /** + * 否则就是所有 Request 接口都放行 + */ + ALL("All"); + + /** + * Request 类型 + */ + private final String type; + + public static RequestMethodEnum find(String type) { + for (RequestMethodEnum value : RequestMethodEnum.values()) { + if (value.getType().equals(type)) { + return value; + } + } + return ALL; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/ResultCode.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/ResultCode.java new file mode 100644 index 0000000000000000000000000000000000000000..13f88154130b931f44d1932a605d473c8485261e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/ResultCode.java @@ -0,0 +1,290 @@ +package tech.hypersense.common.core.enums; + +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import tech.hypersense.common.core.domain.result.base.IResultCode; + +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应码枚举:参考阿里巴巴开发手册响应码规范 + * @Version: 1.0 + */ +@AllArgsConstructor +@NoArgsConstructor +public enum ResultCode implements IResultCode, Serializable { + + SUCCESS("00000", "一切ok"), + + /** 一级宏观错误码 */ + USER_ERROR("A0001", "用户端错误"), + + /** 二级宏观错误码 */ + USER_REGISTRATION_ERROR("A0100", "用户注册错误"), + USER_NOT_AGREE_PRIVACY_AGREEMENT("A0101", "用户未同意隐私协议"), + REGISTRATION_COUNTRY_OR_REGION_RESTRICTED("A0102", "注册国家或地区受限"), + + USERNAME_VERIFICATION_FAILED("A0110", "用户名校验失败"), + USERNAME_ALREADY_EXISTS("A0111", "用户名已存在"), + USERNAME_CONTAINS_SENSITIVE_WORDS("A0112", "用户名包含敏感词"), + USERNAME_CONTAINS_SPECIAL_CHARACTERS("A0113", "用户名包含特殊字符"), + + PASSWORD_VERIFICATION_FAILED("A0120", "密码校验失败"), + PASSWORD_LENGTH_NOT_ENOUGH("A0121", "密码长度不够"), + PASSWORD_STRENGTH_NOT_ENOUGH("A0122", "密码强度不够"), + + VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"), + SMS_VERIFICATION_CODE_INPUT_ERROR("A0131", "短信校验码输入错误"), + EMAIL_VERIFICATION_CODE_INPUT_ERROR("A0132", "邮件校验码输入错误"), + VOICE_VERIFICATION_CODE_INPUT_ERROR("A0133", "语音校验码输入错误"), + + USER_CERTIFICATE_EXCEPTION("A0140", "用户证件异常"), + USER_CERTIFICATE_TYPE_NOT_SELECTED("A0141", "用户证件类型未选择"), + MAINLAND_ID_NUMBER_VERIFICATION_ILLEGAL("A0142", "大陆身份证编号校验非法"), + + USER_BASIC_INFORMATION_VERIFICATION_FAILED("A0150", "用户基本信息校验失败"), + PHONE_FORMAT_VERIFICATION_FAILED("A0151", "手机格式校验失败"), + ADDRESS_FORMAT_VERIFICATION_FAILED("A0152", "地址格式校验失败"), + EMAIL_FORMAT_VERIFICATION_FAILED("A0153", "邮箱格式校验失败"), + + /** 二级宏观错误码 */ + USER_LOGIN_EXCEPTION("A0200", "用户登录异常"), + USER_ACCOUNT_FROZEN("A0201", "用户账户被冻结"), + USER_ACCOUNT_ABOLISHED("A0202", "用户账户已作废"), + + USER_PASSWORD_ERROR("A0210", "用户名或密码错误"), + USER_INPUT_PASSWORD_ERROR_LIMIT_EXCEEDED("A0211", "用户输入密码错误次数超限"), + + USER_IDENTITY_VERIFICATION_FAILED("A0220", "用户身份校验失败"), + USER_FINGERPRINT_RECOGNITION_FAILED("A0221", "用户指纹识别失败"), + USER_FACE_RECOGNITION_FAILED("A0222", "用户面容识别失败"), + USER_NOT_AUTHORIZED_THIRD_PARTY_LOGIN("A0223", "用户未获得第三方登录授权"), + + ACCESS_TOKEN_INVALID("A0230", "访问令牌无效或已过期"), + REFRESH_TOKEN_INVALID("A0231", "刷新令牌无效或已过期"), + + // 验证码错误 + USER_VERIFICATION_CODE_ERROR("A0240", "验证码错误"), + USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"), + USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"), + + /** 二级宏观错误码 */ + ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"), + ACCESS_UNAUTHORIZED("A0301", "访问未授权"), + AUTHORIZATION_IN_PROGRESS("A0302", "正在授权中"), + USER_AUTHORIZATION_APPLICATION_REJECTED("A0303", "用户授权申请被拒绝"), + + ACCESS_OBJECT_PRIVACY_SETTINGS_BLOCKED("A0310", "因访问对象隐私设置被拦截"), + AUTHORIZATION_EXPIRED("A0311", "授权已过期"), + NO_PERMISSION_TO_USE_API("A0312", "无权限使用 API"), + + USER_ACCESS_BLOCKED("A0320", "用户访问被拦截"), + BLACKLISTED_USER("A0321", "黑名单用户"), + ACCOUNT_FROZEN("A0322", "账号被冻结"), + ILLEGAL_IP_ADDRESS("A0323", "非法 IP 地址"), + GATEWAY_ACCESS_RESTRICTED("A0324", "网关访问受限"), + REGION_BLACKLIST("A0325", "地域黑名单"), + + SERVICE_ARREARS("A0330", "服务已欠费"), + + USER_SIGNATURE_EXCEPTION("A0340", "用户签名异常"), + RSA_SIGNATURE_ERROR("A0341", "RSA 签名错误"), + + /** 二级宏观错误码 */ + USER_REQUEST_PARAMETER_ERROR("A0400", "用户请求参数错误"), + CONTAINS_ILLEGAL_MALICIOUS_REDIRECT_LINK("A0401", "包含非法恶意跳转链接"), + INVALID_USER_INPUT("A0402", "无效的用户输入"), + + REQUEST_REQUIRED_PARAMETER_IS_EMPTY("A0410", "请求必填参数为空"), + + REQUEST_PARAMETER_VALUE_EXCEEDS_ALLOWED_RANGE("A0420", "请求参数值超出允许的范围"), + PARAMETER_FORMAT_MISMATCH("A0421", "参数格式不匹配"), + + USER_INPUT_CONTENT_ILLEGAL("A0430", "用户输入内容非法"), + CONTAINS_PROHIBITED_SENSITIVE_WORDS("A0431", "包含违禁敏感词"), + + USER_OPERATION_EXCEPTION("A0440", "用户操作异常"), + + /** 二级宏观错误码 */ + USER_REQUEST_SERVICE_EXCEPTION("A0500", "用户请求服务异常"), + REQUEST_LIMIT_EXCEEDED("A0501", "请求次数超出限制"), + REQUEST_CONCURRENCY_LIMIT_EXCEEDED("A0502", "请求并发数超出限制"), + USER_OPERATION_PLEASE_WAIT("A0503", "用户操作请等待"), + WEBSOCKET_CONNECTION_EXCEPTION("A0504", "WebSocket 连接异常"), + WEBSOCKET_CONNECTION_DISCONNECTED("A0505", "WebSocket 连接断开"), + USER_DUPLICATE_REQUEST("A0506", "请求过于频繁,请稍后再试。"), + + /** 二级宏观错误码 */ + USER_RESOURCE_EXCEPTION("A0600", "用户资源异常"), + ACCOUNT_BALANCE_INSUFFICIENT("A0601", "账户余额不足"), + USER_DISK_SPACE_INSUFFICIENT("A0602", "用户磁盘空间不足"), + USER_MEMORY_SPACE_INSUFFICIENT("A0603", "用户内存空间不足"), + USER_OSS_CAPACITY_INSUFFICIENT("A0604", "用户 OSS 容量不足"), + USER_QUOTA_EXHAUSTED("A0605", "用户配额已用光"), + USER_RESOURCE_NOT_FOUND("A0606", "用户资源不存在"), + + /** 二级宏观错误码 */ + UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"), + UPLOAD_FILE_TYPE_MISMATCH("A0701", "上传文件类型不匹配"), + UPLOAD_FILE_TOO_LARGE("A0702", "上传文件太大"), + UPLOAD_IMAGE_TOO_LARGE("A0703", "上传图片太大"), + UPLOAD_VIDEO_TOO_LARGE("A0704", "上传视频太大"), + UPLOAD_COMPRESSED_FILE_TOO_LARGE("A0705", "上传压缩文件太大"), + + DELETE_FILE_EXCEPTION("A0710", "删除文件异常"), + + /** 二级宏观错误码 */ + USER_CURRENT_VERSION_EXCEPTION("A0800", "用户当前版本异常"), + USER_INSTALLED_VERSION_NOT_MATCH_SYSTEM("A0801", "用户安装版本与系统不匹配"), + USER_INSTALLED_VERSION_TOO_LOW("A0802", "用户安装版本过低"), + USER_INSTALLED_VERSION_TOO_HIGH("A0803", "用户安装版本过高"), + USER_INSTALLED_VERSION_EXPIRED("A0804", "用户安装版本已过期"), + USER_API_REQUEST_VERSION_NOT_MATCH("A0805", "用户 API 请求版本不匹配"), + USER_API_REQUEST_VERSION_TOO_HIGH("A0806", "用户 API 请求版本过高"), + USER_API_REQUEST_VERSION_TOO_LOW("A0807", "用户 API 请求版本过低"), + + /** 二级宏观错误码 */ + USER_PRIVACY_NOT_AUTHORIZED("A0900", "用户隐私未授权"), + USER_PRIVACY_NOT_SIGNED("A0901", "用户隐私未签署"), + USER_CAMERA_NOT_AUTHORIZED("A0903", "用户相机未授权"), + USER_PHOTO_LIBRARY_NOT_AUTHORIZED("A0904", "用户图片库未授权"), + USER_FILE_NOT_AUTHORIZED("A0905", "用户文件未授权"), + USER_LOCATION_INFORMATION_NOT_AUTHORIZED("A0906", "用户位置信息未授权"), + USER_CONTACTS_NOT_AUTHORIZED("A0907", "用户通讯录未授权"), + + /** 二级宏观错误码 */ + USER_DEVICE_EXCEPTION("A1000", "用户设备异常"), + USER_CAMERA_EXCEPTION("A1001", "用户相机异常"), + USER_MICROPHONE_EXCEPTION("A1002", "用户麦克风异常"), + USER_EARPIECE_EXCEPTION("A1003", "用户听筒异常"), + USER_SPEAKER_EXCEPTION("A1004", "用户扬声器异常"), + USER_GPS_POSITIONING_EXCEPTION("A1005", "用户 GPS 定位异常"), + + /** 一级宏观错误码 */ + SYSTEM_ERROR("B0001", "系统执行出错"), + + /** 二级宏观错误码 */ + SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"), + + /** 二级宏观错误码 */ + SYSTEM_DISASTER_RECOVERY_FUNCTION_TRIGGERED("B0200", "系统容灾功能被触发"), + + SYSTEM_RATE_LIMITING("B0210", "系统限流"), + + SYSTEM_FUNCTION_DEGRADATION("B0220", "系统功能降级"), + + /** 二级宏观错误码 */ + SYSTEM_RESOURCE_EXCEPTION("B0300", "系统资源异常"), + SYSTEM_RESOURCE_EXHAUSTED("B0310", "系统资源耗尽"), + SYSTEM_DISK_SPACE_EXHAUSTED("B0311", "系统磁盘空间耗尽"), + SYSTEM_MEMORY_EXHAUSTED("B0312", "系统内存耗尽"), + FILE_HANDLE_EXHAUSTED("B0313", "文件句柄耗尽"), + SYSTEM_CONNECTION_POOL_EXHAUSTED("B0314", "系统连接池耗尽"), + SYSTEM_THREAD_POOL_EXHAUSTED("B0315", "系统线程池耗尽"), + + SYSTEM_RESOURCE_ACCESS_EXCEPTION("B0320", "系统资源访问异常"), + SYSTEM_READ_DISK_FILE_FAILED("B0321", "系统读取磁盘文件失败"), + + + /** 一级宏观错误码 */ + THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"), + + /** 二级宏观错误码 */ + MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"), + + RPC_SERVICE_ERROR("C0110", "RPC 服务出错"), + RPC_SERVICE_NOT_FOUND("C0111", "RPC 服务未找到"), + RPC_SERVICE_NOT_REGISTERED("C0112", "RPC 服务未注册"), + INTERFACE_NOT_EXIST("C0113", "接口不存在"), + + MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"), + MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"), + MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"), + MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"), + MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"), + + CACHE_SERVICE_ERROR("C0130", "缓存服务出错"), + KEY_LENGTH_EXCEEDS_LIMIT("C0131", "key 长度超过限制"), + VALUE_LENGTH_EXCEEDS_LIMIT("C0132", "value 长度超过限制"), + STORAGE_CAPACITY_FULL("C0133", "存储容量已满"), + UNSUPPORTED_DATA_FORMAT("C0134", "不支持的数据格式"), + + CONFIGURATION_SERVICE_ERROR("C0140", "配置服务出错"), + + NETWORK_RESOURCE_SERVICE_ERROR("C0150", "网络资源服务出错"), + VPN_SERVICE_ERROR("C0151", "VPN 服务出错"), + CDN_SERVICE_ERROR("C0152", "CDN 服务出错"), + DOMAIN_NAME_RESOLUTION_SERVICE_ERROR("C0153", "域名解析服务出错"), + GATEWAY_SERVICE_ERROR("C0154", "网关服务出错"), + + /** 二级宏观错误码 */ + THIRD_PARTY_SYSTEM_EXECUTION_TIMEOUT("C0200", "第三方系统执行超时"), + + RPC_EXECUTION_TIMEOUT("C0210", "RPC 执行超时"), + + MESSAGE_DELIVERY_TIMEOUT("C0220", "消息投递超时"), + + CACHE_SERVICE_TIMEOUT("C0230", "缓存服务超时"), + + CONFIGURATION_SERVICE_TIMEOUT("C0240", "配置服务超时"), + + DATABASE_SERVICE_TIMEOUT("C0250", "数据库服务超时"), + + /** 二级宏观错误码 */ + DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"), + + TABLE_NOT_EXIST("C0311", "表不存在"), + COLUMN_NOT_EXIST("C0312", "列不存在"), + + MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"), + + DATABASE_DEADLOCK("C0331", "数据库死锁"), + + PRIMARY_KEY_CONFLICT("C0341", "主键冲突"), + + /** 二级宏观错误码 */ + THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"), + THIRD_PARTY_SYSTEM_RATE_LIMITING("C0401", "第三方系统限流"), + THIRD_PARTY_FUNCTION_DEGRADATION("C0402", "第三方功能降级"), + + /** 二级宏观错误码 */ + NOTIFICATION_SERVICE_ERROR("C0500", "通知服务出错"), + SMS_REMINDER_SERVICE_FAILED("C0501", "短信提醒服务失败"), + VOICE_REMINDER_SERVICE_FAILED("C0502", "语音提醒服务失败"), + EMAIL_REMINDER_SERVICE_FAILED("C0503", "邮件提醒服务失败"); + + + @Override + public String getCode() { + return code; + } + + @Override + public String getMsg() { + return msg; + } + + private String code; + + private String msg; + + @Override + public String toString() { + return "{" + + "\"code\":\"" + code + '\"' + + ", \"msg\":\"" + msg + '\"' + + '}'; + } + + + public static ResultCode getValue(String code) { + for (ResultCode value : values()) { + if (value.getCode().equals(code)) { + return value; + } + } + return SYSTEM_ERROR; // 默认系统执行错误 + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/StatusEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/StatusEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..0c6394a668cade200f7e4384bf982219d0e6aa83 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/StatusEnum.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.core.enums; + +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 状态枚举 + * @Version: 1.0 + */ +@Getter +public enum StatusEnum implements IBaseEnum { + + ENABLE(1, "启用"), + DISABLE (0, "禁用"); + + private final Integer value; + + + private final String label; + + StatusEnum(Integer value, String label) { + this.value = value; + this.label = label; + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/base/IBaseEnum.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/base/IBaseEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..b647205c0607d4276629214431504f4d529baef2 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/enums/base/IBaseEnum.java @@ -0,0 +1,85 @@ +package tech.hypersense.common.core.enums.base; + +import cn.hutool.core.util.ObjectUtil; + +import java.util.EnumSet; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 枚举通用接口 + * @Version: 1.0 + */ +public interface IBaseEnum{ + + T getValue(); + + String getLabel(); + + /** + * 根据值获取枚举 + * + * @param value + * @param clazz + * @param 枚举 + * @return + */ + static & IBaseEnum> E getEnumByValue(Object value, Class clazz) { + Objects.requireNonNull(value); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getValue(), value)) + .findFirst() + .orElse(null); + return matchEnum; + } + + /** + * 根据文本标签获取值 + * + * @param value + * @param clazz + * @param + * @return + */ + static & IBaseEnum> String getLabelByValue(Object value, Class clazz) { + Objects.requireNonNull(value); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getValue(), value)) + .findFirst() + .orElse(null); + + String label = null; + if (matchEnum != null) { + label = matchEnum.getLabel(); + } + return label; + } + + + /** + * 根据文本标签获取值 + * + * @param label + * @param clazz + * @param + * @return + */ + static & IBaseEnum> Object getValueByLabel(String label, Class clazz) { + Objects.requireNonNull(label); + EnumSet allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举 + String finalLabel = label; + E matchEnum = allEnums.stream() + .filter(e -> ObjectUtil.equal(e.getLabel(), finalLabel)) + .findFirst() + .orElse(null); + + Object value = null; + if (matchEnum != null) { + value = matchEnum.getValue(); + } + return value; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/common/log/OperateLogEvent.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/common/log/OperateLogEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..fcc462cb6968402df3f3f79e9acc0c27267743d6 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/common/log/OperateLogEvent.java @@ -0,0 +1,101 @@ +package tech.hypersense.common.core.event.common.log; + +import lombok.Data; +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 操作日志事件 + * @Version: 1.0 + */ +@Data +public class OperateLogEvent implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 主键 + */ + private Long id; + + /** + * 日志模块 + */ + private LogModuleEnum module; + + /** + * 请求方式 + */ + private String requestMethod; + + /** + * 请求参数 + */ + private String requestParams; + + /** + * 响应参数 + */ + private String responseContent; + + /** + * 日志内容 + */ + private String content; + + /** + * 请求路径 + */ + private String requestUri; + + /** + * IP 地址 + */ + private String ip; + + /** + * 省份 + */ + private String province; + + /** + * 城市 + */ + private String city; + + /** + * 浏览器 + */ + private String browser; + + /** + * 浏览器版本 + */ + private String browserVersion; + + /** + * 终端系统 + */ + private String os; + + /** + * 执行时间(毫秒) + */ + private Long executionTime; + + /** + * 创建人ID + */ + private Long createBy; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/moudules/system/UserConnectionEvent.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/moudules/system/UserConnectionEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..874bc14479ac668fc6ae6dbb4044b28695108773 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/event/moudules/system/UserConnectionEvent.java @@ -0,0 +1,37 @@ +package tech.hypersense.common.core.event.moudules.system; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户连接事件 + * @Version: 1.0 + */ +@Getter +public class UserConnectionEvent extends ApplicationEvent { + + /** + * 用户名 + */ + private final String username; + + /** + * 是否连接 + */ + private final boolean connected; + + /** + * 用户连接事件 + * + * @param source 事件源 + * @param username 用户名 + * @param connected 是否连接 + */ + public UserConnectionEvent(Object source, String username, boolean connected) { + super(source); + this.username = username; + this.connected = connected; + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/BusinessException.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/BusinessException.java new file mode 100644 index 0000000000000000000000000000000000000000..511c2688604c5466607d9eff7851e83f332c9ed2 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/BusinessException.java @@ -0,0 +1,46 @@ +package tech.hypersense.common.core.exception; + +import lombok.Getter; +import org.slf4j.helpers.MessageFormatter; +import tech.hypersense.common.core.domain.result.base.IResultCode; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 自定义业务异常 + * @Version: 1.0 + */ +@Getter +public class BusinessException extends RuntimeException { + + public IResultCode resultCode; + + public BusinessException(IResultCode errorCode) { + super(errorCode.getMsg()); + this.resultCode = errorCode; + } + + + public BusinessException(IResultCode errorCode,String message) { + super(message); + this.resultCode = errorCode; + } + + + public BusinessException(String message, Throwable cause) { + super(message, cause); + } + + public BusinessException(Throwable cause) { + super(cause); + } + + public BusinessException(String message, Object... args) { + super(formatMessage(message, args)); + } + + private static String formatMessage(String message, Object... args) { + return MessageFormatter.arrayFormat(message, args).getMessage(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/handler/GlobalExceptionHandler.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..4fdf631e4255fefeebb1cbe3868332601f885b78 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,267 @@ +package tech.hypersense.common.core.exception.handler; + +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.ServletException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; + +import java.sql.SQLSyntaxErrorException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + + /** + * 处理绑定异常 + * + * 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(BindException e) { + log.error("BindException:{}", e.getMessage()); + String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg); + } + + /** + * 处理 @RequestParam 参数校验异常 + * + * 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过), + * 会捕获到 ConstraintViolationException 异常。 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ConstraintViolationException e) { + log.error("ConstraintViolationException:{}", e.getMessage()); + String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理方法参数校验异常 + * + * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, + * 会抛出 MethodArgumentNotValidException 异常。 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException:{}", e.getMessage()); + String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理接口不存在的异常 + * + * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result processException(NoHandlerFoundException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.INTERFACE_NOT_EXIST); + } + + /** + * 处理缺少请求参数的异常 + * + * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); + } + + /** + * 处理方法参数类型不匹配的异常 + * + * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(BindException e) { + log.error("BindException:{}", e.getMessage()); + String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg); + } + + /** + * 处理 @RequestParam 参数校验异常 + * + * 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过), + * 会捕获到 ConstraintViolationException 异常。 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ConstraintViolationException e) { + log.error("ConstraintViolationException:{}", e.getMessage()); + String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理方法参数校验异常 + * + * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, + * 会抛出 MethodArgumentNotValidException 异常。 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException:{}", e.getMessage()); + String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理接口不存在的异常 + * + * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result processException(NoHandlerFoundException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.INTERFACE_NOT_EXIST); + } + + /** + * 处理缺少请求参数的异常 + * + * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); + } + + /** + * 处理方法参数类型不匹配的异常 + * + * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过), + * 会捕获到 ConstraintViolationException 异常。 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ConstraintViolationException e) { + log.error("ConstraintViolationException:{}", e.getMessage()); + String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理方法参数校验异常 + * + * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, + * 会抛出 MethodArgumentNotValidException 异常。 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException:{}", e.getMessage()); + String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理接口不存在的异常 + * + * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result processException(NoHandlerFoundException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.INTERFACE_NOT_EXIST); + } + + /** + * 处理缺少请求参数的异常 + * + * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); + } + + /** + * 处理方法参数类型不匹配的异常 + * + * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, + * 会抛出 MethodArgumentNotValidException 异常。 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException:{}", e.getMessage()); + String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";")); + return Result.failed(ResultCode.INVALID_USER_INPUT, msg); + } + + /** + * 处理接口不存在的异常 + * + * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result processException(NoHandlerFoundException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.INTERFACE_NOT_EXIST); + } + + /** + * 处理缺少请求参数的异常 + * + * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); + } + + /** + * 处理方法参数类型不匹配的异常 + * + * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 + */ + @ExceptionHandler(NoHandlerFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Result processException(NoHandlerFoundException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.INTERFACE_NOT_EXIST); + } + + /** + * 处理缺少请求参数的异常 + * + * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); + } + + /** + * 处理方法参数类型不匹配的异常 + * + * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MissingServletRequestParameterException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); + } + + /** + * 处理方法参数类型不匹配的异常 + * + * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(MethodArgumentTypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); + } + + /** + * 处理 Servlet 异常 + * + * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 + */ + @ExceptionHandler(ServletException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(ServletException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理非法参数异常 + * + * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 JSON 处理异常 + * + * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当处理 JSON 数据时发生错误,会抛出 JsonProcessingException 异常。 + */ + @ExceptionHandler(JsonProcessingException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleJsonProcessingException(JsonProcessingException e) { + log.error("Json转换异常,异常原因:{}", e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理请求体不可读的异常 + * + * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(HttpMessageNotReadableException e) { + log.error(e.getMessage(), e); + String errorMessage = "请求体不可为空"; + Throwable cause = e.getCause(); + if (cause != null) { + errorMessage = convertMessage(cause); + } + return Result.failed(errorMessage); + } + + /** + * 处理类型不匹配异常 + * + * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 + */ + @ExceptionHandler(TypeMismatchException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result processException(TypeMismatchException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 + */ + @ExceptionHandler(BadSqlGrammarException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleBadSqlGrammarException(BadSqlGrammarException e) { + log.error(e.getMessage(), e); + String errorMsg = e.getMessage(); + if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { + return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + } else { + return Result.failed(e.getMessage()); + } + } + + /** + * 处理 SQL 语法错误异常 + * + * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 + */ + @ExceptionHandler(SQLSyntaxErrorException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { + log.error(e.getMessage(), e); + return Result.failed(e.getMessage()); + } + + /** + * 处理业务异常 + * + * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBizException(BusinessException e) { + log.error("biz exception", e); + if (e.getResultCode() != null) { + return Result.failed(e.getResultCode(), e.getMessage()); + } + return Result.failed(e.getMessage()); + } + + /** + * 处理所有未捕获的异常 + * + * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 当发生未捕获的异常时,会抛出 Exception 异常。 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleException(Exception e) throws Exception { + // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理 + if (e instanceof AccessDeniedException + || e instanceof AuthenticationException) { + throw e; + } + log.error("unknown exception", e); + return Result.failed(e.getLocalizedMessage()); + } + + /** + * 传参类型错误时,用于消息转换 + * + * @param throwable 异常 + * @return 错误信息 + */ + private String convertMessage(Throwable throwable) { + String error = throwable.toString(); + String regulation = "\\[\"(.*?)\"]+"; + Pattern pattern = Pattern.compile(regulation); + Matcher matcher = pattern.matcher(error); + String group = ""; + if (matcher.find()) { + String matchString = matcher.group(); + matchString = matchString.replace("[", "").replace("]", ""); + matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", "")); + group += matchString; + } + return group; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..8cd1a0064740aaf1b2814ff5c3439037a324c588 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/DateUtils.java @@ -0,0 +1,59 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.format.annotation.DateTimeFormat; + +import java.lang.reflect.Field; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 日期工具类 + * @Version: 1.0 + */ +public class DateUtils { + + /** + * 区间日期格式化为数据库日期格式 + * + * @param obj 要处理的对象 + * @param startTimeFieldName 起始时间字段名 + * @param endTimeFieldName 结束时间字段名 + */ + public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) { + Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName); + Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName); + + if (startTimeField != null) { + processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00"); + } + + if (endTimeField != null) { + processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59"); + } + } + + /** + * 处理日期字段 + * + * @param obj 要处理的对象 + * @param field 字段 + * @param fieldName 字段名 + * @param targetPattern 目标数据库日期格式 + */ + private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) { + Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName); + if (fieldValue != null) { + // 得到原始的日期格式 + String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd"; + // 转换为日期对象 + DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern); + // 转换为目标数据库日期格式 + ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern)); + } + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..3470ab74d87367a724915b2e6558a47c986ce082 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ExcelUtils.java @@ -0,0 +1,19 @@ +package tech.hypersense.common.core.utils; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.event.AnalysisEventListener; + +import java.io.InputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: Excel 工具类 + * @Version: 1.0 + */ +public class ExcelUtils { + + public static void importExcel(InputStream is, Class clazz, AnalysisEventListener listener) { + EasyExcel.read(is, clazz, listener).sheet().doRead(); + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c2b5deda1ba56c04f292a665cd38a68b87f62ed8 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/IPUtils.java @@ -0,0 +1,138 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.util.StrUtil; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; +import org.springframework.stereotype.Component; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: IP工具类 + * + * 获取客户端IP地址和IP地址对应的地理位置信息 + * + * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + * 参考文档:mapstruct-plus + * + * @author Michelle.Chung + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MapstructUtils { + + private final static Converter CONVERTER = SpringUtils.getBean(Converter.class); + + /** + * 将 T 类型对象,转换为 desc 类型的对象并返回 + * + * @param source 数据来源实体 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static V convert(T source, Class desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型对象,按照配置的映射字段规则,给 desc 类型的对象赋值并返回 desc 对象 + * + * @param source 数据来源实体 + * @param desc 转换后的对象 + * @return desc + */ + public static V convert(T source, V desc) { + if (ObjectUtil.isNull(source)) { + return null; + } + if (ObjectUtil.isNull(desc)) { + return null; + } + return CONVERTER.convert(source, desc); + } + + /** + * 将 T 类型的集合,转换为 desc 类型的集合并返回 + * + * @param sourceList 数据来源实体列表 + * @param desc 描述对象 转换后的对象 + * @return desc + */ + public static List convert(List sourceList, Class desc) { + if (ObjectUtil.isNull(sourceList)) { + return null; + } + if (CollUtil.isEmpty(sourceList)) { + return CollUtil.newArrayList(); + } + return CONVERTER.convert(sourceList, desc); + } + + /** + * 将 Map 转换为 beanClass 类型的集合并返回 + * + * @param map 数据来源 + * @param beanClass bean类 + * @return bean对象 + */ + public static T convert(Map map, Class beanClass) { + if (MapUtil.isEmpty(map)) { + return null; + } + if (ObjectUtil.isNull(beanClass)) { + return null; + } + return CONVERTER.convert(map, beanClass); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..30ceae7bf260b43bd96c8099a62b69091f40e330 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/ResponseUtils.java @@ -0,0 +1,80 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.ResultCode; + +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 响应工具类 + * @Version: 1.0 + */ +@Slf4j +public class ResponseUtils { + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + /** + * 异常消息返回(适用过滤器中处理异常响应) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { + int status = getHttpStatus(resultCode); + + response.setStatus(status); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + try (PrintWriter writer = response.getWriter()) { + String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); + writer.print(jsonResponse); + writer.flush(); // 确保将响应内容写入到输出流 + } catch (IOException e) { + log.error("响应异常处理失败", e); + } + } + + + /** + * 根据结果码获取HTTP状态码 + * + * @param resultCode 结果码 + * @return HTTP状态码 + */ + private static int getHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..cdb436dd8122971274a9f8aa58891ef675b5ac4a --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/SpringUtils.java @@ -0,0 +1,63 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.aop.framework.AopContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: spring工具类 + * @Version: 1.0 + */ +@Component +public final class SpringUtils extends SpringUtil { + + /** + * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true + */ + public static boolean containsBean(String name) { + return getBeanFactory().containsBean(name); + } + + /** + * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 + * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException) + */ + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().isSingleton(name); + } + + /** + * @return Class 注册对象的类型 + */ + public static Class> getType(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getType(name); + } + + /** + * 如果给定的bean名字在bean定义中有别名,则返回这些别名 + */ + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return getBeanFactory().getAliases(name); + } + + /** + * 获取aop代理对象 + */ + @SuppressWarnings("unchecked") + public static T getAopProxy(T invoker) { + return (T) AopContext.currentProxy(); + } + + + /** + * 获取spring上下文 + */ + public static ApplicationContext context() { + return getApplicationContext(); + } + +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..b450f71c043af23556f8cb02e8395ff254f9670b --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/validator/FieldValidator.java @@ -0,0 +1,34 @@ +package tech.hypersense.common.core.validator; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import tech.hypersense.common.core.annotation.ValidField; + +import java.util.Arrays; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字段校验器 + * @Version: 1.0 + */ +public class FieldValidator implements ConstraintValidator { + + private String[] allowedValues; + + @Override + public void initialize(ValidField constraintAnnotation) { + // 初始化允许的值列表 + this.allowedValues = constraintAnnotation.allowedValues(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 如果字段允许为空,可以返回 true + } + // 检查值是否在允许列表中 + return Arrays.asList(allowedValues).contains(value); + } +} + diff --git a/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..79048db23955cca877e01956bbf9cc511d822d39 --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ + +tech.hypersense.common.core.config.CaffeineConfig +tech.hypersense.common.core.exception.handler.GlobalExceptionHandler +tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler \ No newline at end of file diff --git a/hypersense-common/hypersense-common-doc/pom.xml b/hypersense-common/hypersense-common-doc/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5f6cbb05f14bf2e972ac76e093a8831e2cd79aac --- /dev/null +++ b/hypersense-common/hypersense-common-doc/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-doc + Archetype - hypersense-common-doc + + hypersense-common-doc 接口文档服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + diff --git a/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..009584f2611adea7af72ba3c4c0f4cdb4ff9638e --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/java/tech/hypersense/common/doc/config/OpenApiConfig.java @@ -0,0 +1,108 @@ +package tech.hypersense.common.doc.config; + +import cn.hutool.core.util.ArrayUtil; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpHeaders; +import org.springframework.util.AntPathMatcher; +import tech.hypersense.common.security.config.properties.SecurityProperties; + +import java.util.stream.Stream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: OpenAPI 接口文档配置 + * @Version: 1.0 + */ +@Configuration +@RequiredArgsConstructor +@Slf4j +public class OpenApiConfig { + + private final Environment environment; + + @Resource + private final SecurityProperties securityProperties; + + /** + * 接口文档信息 + */ + @Bean + public OpenAPI openApi() { + + String appVersion = environment.getProperty("project.version", "1.0.0"); + + return new OpenAPI() + .info(new Info() + .title("管理系统 API 文档") + .description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") + .version(appVersion) + .license(new License() + .name("Apache License 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0") + ) + .contact(new Contact() + .name("youlai") + .email("youlaitech@163.com") + .url("https://www.youlai.tech") + ) + ) + // 配置全局鉴权参数-Authorize + .components(new Components() + .addSecuritySchemes(HttpHeaders.AUTHORIZATION, + new SecurityScheme() + .name(HttpHeaders.AUTHORIZATION) + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .scheme("Bearer") + .bearerFormat("JWT") + ) + ); + } + + + /** + * 全局自定义扩展 + */ + @Bean + public GlobalOpenApiCustomizer globalOpenApiCustomizer() { + return openApi -> { + // 全局添加Authorization + if (openApi.getPaths() != null) { + openApi.getPaths().forEach((path, pathItem) -> { + + // 忽略认证的请求无需携带 Authorization + String[] ignoreUrls = securityProperties.getIgnoreUrls(); + if (ArrayUtil.isNotEmpty(ignoreUrls)) { + // Ant 匹配忽略的路径,不添加Authorization + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { + return; + } + } + + // 其他接口统一添加Authorization + pathItem.readOperations() + .forEach(operation -> + operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) + ); + }); + } + }; + } + +} + diff --git a/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..487f8926af55fc1c2c55eeeb64c893bdb7be89a5 --- /dev/null +++ b/hypersense-common/hypersense-common-doc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +tech.hypersense.common.doc.config.OpenApiConfig diff --git a/hypersense-common/hypersense-common-log/pom.xml b/hypersense-common/hypersense-common-log/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0d1270e329f96a516ceba6ae135ecf45aa8e69b8 --- /dev/null +++ b/hypersense-common/hypersense-common-log/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-log + + hypersense-common-log 日志服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..3f9497c300e3b59f54da004fd0d5699b88fcc5f0 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/annotation/Log.java @@ -0,0 +1,49 @@ +package tech.hypersense.common.log.annotation; + +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志注解 + * @Version: 1.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Log { + + /** + * 日志描述 + * + * @return 日志描述 + */ + String value() default ""; + + /** + * 日志模块 + * + * @return 日志模块 + */ + + LogModuleEnum module(); + + /** + * 是否记录请求参数 + * + * @return 是否记录请求参数 + */ + boolean params() default true; + + /** + * 是否记录响应结果 + * + * 响应结果默认不记录,避免日志过大 + * @return 是否记录响应结果 + */ + boolean result() default false; + + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..5578aef7879254808ff6f60e91ddf88881c6c1d4 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/aspect/LogAspect.java @@ -0,0 +1,232 @@ +package tech.hypersense.common.log.aspect; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.HandlerMapping; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.core.utils.SpringUtils; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 日志切面 + * @Version: 1.0 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LogAspect { + private final HttpServletRequest request; + private final CacheManager cacheManager; + + /** + * 切点 + */ + @Pointcut("@annotation(tech.hypersense.common.log.annotation.Log)") + public void logPointcut() { + } + + /** + * 处理完请求后执行 + * + * @param joinPoint 切点 + */ + @AfterReturning(pointcut = "logPointcut() && @annotation(logAnnotation)", returning = "jsonResult") + public void doAfterReturning(JoinPoint joinPoint, tech.hypersense.common.log.annotation.Log logAnnotation, Object jsonResult) { + this.saveLog(joinPoint, null, jsonResult, logAnnotation); + } + + + /** + * 拦截异常操作 + * + * @param joinPoint 切点 + * @param e 异常 + */ + @AfterThrowing(value = "logPointcut()", throwing = "e") + public void doAfterThrowing(JoinPoint joinPoint, Exception e) { + this.saveLog(joinPoint, e, null, null); + } + + /** + * 保存日志 + * + * @param joinPoint 切点 + * @param e 异常 + * @param jsonResult 响应结果 + * @param logAnnotation 日志注解 + */ + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, tech.hypersense.common.log.annotation.Log logAnnotation) { + String requestURI = request.getRequestURI(); + + TimeInterval timer = DateUtil.timer(); + // 执行方法 + long executionTime = timer.interval(); + + // *========数据库日志=========*// + OperateLogEvent operateLogEvent = new OperateLogEvent(); + if (logAnnotation == null && e != null) { + operateLogEvent.setModule(LogModuleEnum.EXCEPTION); + operateLogEvent.setContent("系统发生异常"); + this.setRequestParameters(joinPoint, operateLogEvent); + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace())); + } else { + operateLogEvent.setModule(logAnnotation.module()); + operateLogEvent.setContent(logAnnotation.value()); + // 请求参数 + if (logAnnotation.params()) { + this.setRequestParameters(joinPoint, operateLogEvent); + } + // 响应结果 + if (logAnnotation.result() && jsonResult != null) { + operateLogEvent.setResponseContent(JSONUtil.toJsonStr(jsonResult)); + } + } + operateLogEvent.setRequestUri(requestURI); + Long userId = SecurityUtils.getUserId(); + operateLogEvent.setCreateBy(userId); + String ipAddr = IPUtils.getIpAddr(request); + if (StrUtil.isNotBlank(ipAddr)) { + operateLogEvent.setIp(ipAddr); + String region = IPUtils.getRegion(ipAddr); + // 中国|0|四川省|成都市|电信 解析省和市 + if (StrUtil.isNotBlank(region)) { + String[] regionArray = region.split("\\|"); + if (regionArray.length > 2) { + operateLogEvent.setProvince(regionArray[2]); + operateLogEvent.setCity(regionArray[3]); + } + } + } + + operateLogEvent.setExecutionTime(executionTime); + // 获取浏览器和终端系统信息 + String userAgentString = request.getHeader("User-Agent"); + UserAgent userAgent = resolveUserAgent(userAgentString); + if (Objects.nonNull(userAgent)) { + // 系统信息 + operateLogEvent.setOs(userAgent.getOs().getName()); + // 浏览器信息 + operateLogEvent.setBrowser(userAgent.getBrowser().getName()); + operateLogEvent.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString)); + } + + // 发布事件保存数据库 + SpringUtils.context().publishEvent(operateLogEvent); + + + } + + /** + * 设置请求参数到日志对象中 + * + * @param joinPoint 切点 + * @param operateLogEvent 操作日志 + */ + private void setRequestParameters(JoinPoint joinPoint, OperateLogEvent operateLogEvent) { + String requestMethod = request.getMethod(); + operateLogEvent.setRequestMethod(requestMethod); + if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) { + String params = convertArgumentsToString(joinPoint.getArgs()); + operateLogEvent.setRequestParams(StrUtil.sub(params, 0, 65535)); + } else { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + Map, ?> paramsMap = (Map, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + operateLogEvent.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535)); + } else { + operateLogEvent.setRequestParams(""); + } + } + } + + /** + * 将参数数组转换为字符串 + * + * @param paramsArray 参数数组 + * @return 参数字符串 + */ + private String convertArgumentsToString(Object[] paramsArray) { + StringBuilder params = new StringBuilder(); + if (paramsArray != null) { + for (Object param : paramsArray) { + if (!shouldFilterObject(param)) { + params.append(JSONUtil.toJsonStr(param)).append(" "); + } + } + } + return params.toString().trim(); + } + + /** + * 判断是否需要过滤的对象。 + * + * @param obj 对象信息。 + * @return 如果是需要过滤的对象,则返回true;否则返回false。 + */ + private boolean shouldFilterObject(Object obj) { + Class> clazz = obj.getClass(); + if (clazz.isArray()) { + return MultipartFile.class.isAssignableFrom(clazz.getComponentType()); + } else if (Collection.class.isAssignableFrom(clazz)) { + Collection> collection = (Collection>) obj; + return collection.stream().anyMatch(item -> item instanceof MultipartFile); + } else if (Map.class.isAssignableFrom(clazz)) { + Map, ?> map = (Map, ?>) obj; + return map.values().stream().anyMatch(value -> value instanceof MultipartFile); + } + return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse; + } + + + /** + * 解析UserAgent + * + * @param userAgentString UserAgent字符串 + * @return UserAgent + */ + public UserAgent resolveUserAgent(String userAgentString) { + if (StrUtil.isBlank(userAgentString)) { + return null; + } + // 给userAgentStringMD5加密一次防止过长 + String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString); + //判断是否命中缓存 + UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class); + if (userAgent != null) { + return userAgent; + } + userAgent = UserAgentUtil.parse(userAgentString); + Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent); + return userAgent; + } + +} diff --git a/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..0356d75a0653a5f686ee712d176d04dec060d145 --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/java/tech/hypersense/common/log/filter/RequestLogFilter.java @@ -0,0 +1,39 @@ +package tech.hypersense.common.log.filter; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; +import tech.hypersense.common.core.utils.IPUtils; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 请求日志打印过滤器 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class RequestLogFilter extends CommonsRequestLoggingFilter { + + @Override + protected boolean shouldLog(HttpServletRequest request) { + // 设置日志输出级别,默认debug + return this.logger.isInfoEnabled(); + } + + @Override + protected void beforeRequest(HttpServletRequest request, String message) { + String requestURI = request.getRequestURI(); + String ip = IPUtils.getIpAddr(request); + log.info("request,ip:{}, uri: {}", ip, requestURI); + super.beforeRequest(request, message); + } + + @Override + protected void afterRequest(HttpServletRequest request, String message) { + super.afterRequest(request, message); + } + +} + diff --git a/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..ba372e010e9bc80d0166b0717321c8971894b10f --- /dev/null +++ b/hypersense-common/hypersense-common-log/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +tech.hypersense.common.log.aspect.LogAspect +tech.hypersense.common.log.filter.RequestLogFilter + diff --git a/hypersense-common/hypersense-common-mybatis/pom.xml b/hypersense-common/hypersense-common-mybatis/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1367f9f45af412e0ae95875e4f6f29d0b9a9a89 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + 1.0.0 + + hypersense-common-mybatis + + hypersense-common-mybatis 数据库服务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-security + + + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..584992cf515e7f21f027d68a5b1a37ea794d33bb --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/config/MybatisConfig.java @@ -0,0 +1,48 @@ +package tech.hypersense.common.mybatis.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.hypersense.common.core.domain.handler.CommonMetaObjectHandler; +import tech.hypersense.common.mybatis.handler.MyDataPermissionHandler; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: mybatis-plus 配置类 + * @Version: 1.0 + */ +@Configuration +@EnableTransactionManagement +public class MybatisConfig { + + /** + * 分页插件和数据权限插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + //数据权限 + interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler())); + //分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + + return interceptor; + } + + /** + * 自动填充数据库创建人、创建时间、更新人、更新时间 + */ + @Bean + public GlobalConfig globalConfig() { + GlobalConfig globalConfig = new GlobalConfig(); + globalConfig.setMetaObjectHandler(new CommonMetaObjectHandler()); + return globalConfig; + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..0200f17d214871fbcae7f4b3311add3630402a78 --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/java/tech/hypersense/common/mybatis/handler/MyDataPermissionHandler.java @@ -0,0 +1,115 @@ +package tech.hypersense.common.mybatis.handler; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import tech.hypersense.common.core.enums.DataScopeEnum; +import tech.hypersense.common.core.enums.base.IBaseEnum; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.lang.reflect.Method; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据权限控制器 + * @Version: 1.0 + */ +@Slf4j +public class MyDataPermissionHandler implements DataPermissionHandler { + + /** + * 获取数据权限的sql片段 + * @param where 查询条件 + * @param mappedStatementId mapper接口方法的全路径 + * @return sql片段 + */ + @Override + @SneakyThrows + public Expression getSqlSegment(Expression where, String mappedStatementId) { + // 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回 + if(SecurityUtils.getUserId() == null || SecurityUtils.isRoot()){ + return where; + } + // 获取当前用户的数据权限 + Integer dataScope = SecurityUtils.getDataScope(); + DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class); + // 如果是全部数据权限,直接返回 + if (DataScopeEnum.ALL.equals(dataScopeEnum)) { + return where; + } + // 获取当前执行的接口类 + Class> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT))); + // 获取当前执行的方法名称 + String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1); + // 获取当前执行的接口类里所有的方法 + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + //找到当前执行的方法 + if (method.getName().equals(methodName)) { + DataPermission annotation = method.getAnnotation(DataPermission.class); + // 判断当前执行的方法是否有权限注解,如果没有注解直接返回 + if (annotation == null ) { + return where; + } + return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), dataScopeEnum,where); + } + } + return where; + } + + /** + * 构建过滤条件 + * + * @param where 当前查询条件 + * @return 构建后查询条件 + */ + @SneakyThrows + public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName,DataScopeEnum dataScopeEnum, Expression where) { + + // 获取部门和用户的别名 + String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName; + String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName; + + Long deptId, userId; + String appendSqlStr; + switch (dataScopeEnum) { + case ALL: + return where; + case DEPT: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + StringPool.EQUALS + deptId; + break; + case SELF: + userId = SecurityUtils.getUserId(); + appendSqlStr = userColumnName + StringPool.EQUALS + userId; + break; + // 默认部门及子部门数据权限 + default: + deptId = SecurityUtils.getDeptId(); + appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )"; + break; + } + + if (StrUtil.isBlank(appendSqlStr)) { + return where; + } + + Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr); + + if (where == null) { + return appendExpression; + } + + return new AndExpression(where, appendExpression); + } + + +} + diff --git a/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..59c08d65747d402ab2b74519a8ba78315c939e1c --- /dev/null +++ b/hypersense-common/hypersense-common-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.mybatis.config.MybatisConfig +tech.hypersense.common.mybatis.handler.MyDataPermissionHandler + + diff --git a/hypersense-common/hypersense-common-security/pom.xml b/hypersense-common/hypersense-common-security/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..32a3de07536d4e81bdc49945852415ce8289fc25 --- /dev/null +++ b/hypersense-common/hypersense-common-security/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-security + + + hypersense-common-security 安全服务模块 + + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + + tech.hypersense + hypersense-shared-redis + ${revision} + + + + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed96c9133b6b4a8cdf1988c9e22d8f0f8a70a73 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/PasswordEncoderConfig.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.security.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 密码编码器 + * @Version: 1.0 + */ +@Configuration +public class PasswordEncoderConfig { + + /** + * 密码编码器 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..4134b849c34f8e3e992b0a33b7f9b396cec5e198 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/config/properties/SecurityProperties.java @@ -0,0 +1,112 @@ +package tech.hypersense.common.security.config.properties; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 安全模块配置属性类 + * + * 映射 application.yml 中 security 前缀的安全相关配置 + * @Version: 1.0 + */ +@Data +@Component +@Validated +@ConfigurationProperties(prefix = "security") +public class SecurityProperties { + + /** + * 会话管理配置 + */ + private SessionConfig session; + + /** + * 安全白名单路径(完全绕过安全过滤器) + * 示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + * 示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + * + * jwt - 基于JWT的无状态认证 + * redis-token - 基于Redis的有状态认证 + * + */ + @NotNull + private String type; + + /** + * 访问令牌有效期(单位:秒) + * 默认值:3600(1小时) + * -1 表示永不过期 + */ + @Min(-1) + private Integer accessTokenTimeToLive = 3600; + + /** + * 刷新令牌有效期(单位:秒) + * 默认值:604800(7天) + * -1 表示永不过期 + */ + @Min(-1) + private Integer refreshTokenTimeToLive = 604800; + + /** + * JWT 配置项 + */ + private JwtConfig jwt; + + /** + * Redis令牌配置项 + */ + private RedisTokenConfig redisToken; + } + + /** + * JWT 配置嵌套类 + */ + @Data + public static class JwtConfig { + /** + * JWT签名密钥 + * HS256算法要求至少32个字符 + * 示例:SecretKey012345678901234567890123456789 + */ + @NotNull + private String secretKey; + } + + /** + * Redis令牌配置嵌套类 + */ + @Data + public static class RedisTokenConfig { + /** + * 是否允许多设备同时登录 + * true - 允许同一账户多设备登录(默认) + * false - 新登录会使旧令牌失效 + */ + private Boolean allowMultiLogin = true; + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..6a1e22930b1ecd97ca20635e8d4bca182131657c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/exception/CaptchaValidationException.java @@ -0,0 +1,15 @@ +package tech.hypersense.common.security.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码校验异常 + * @Version: 1.0 + */ +public class CaptchaValidationException extends AuthenticationException { + public CaptchaValidationException(String msg) { + super(msg); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java new file mode 100644 index 0000000000000000000000000000000000000000..444f4995f633392bd9982c1c281d9cad7791692d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/AuthenticationToken.java @@ -0,0 +1,30 @@ +package tech.hypersense.common.security.model; + +import lombok.Builder; +import lombok.Data; +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 认证令牌响应对象 + * @Version: 1.0 + */ +@Schema(description = "认证令牌响应对象") +@Data +@Builder +public class AuthenticationToken { + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "过期时间(单位:秒)") + private Integer expiresIn; + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java new file mode 100644 index 0000000000000000000000000000000000000000..14491f2061052790b9eb7621f4442813feefe2a3 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/OnlineUser.java @@ -0,0 +1,45 @@ +package tech.hypersense.common.security.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户信息对象 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OnlineUser { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + * 定义用户可访问的数据范围,如全部、本部门或自定义范围 + */ + private Integer dataScope; + + /** + * 角色权限集合 + */ + private Set authorities; +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0bc7a52d679429f7ef87558974f6195735f37e --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/model/SysUserDetails.java @@ -0,0 +1,109 @@ +package tech.hypersense.common.security.model; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * Spring Security 用户认证对象 + * + * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 获取客户端IP地址和IP地址对应的地理位置信息 + *
+ * 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 + * 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 + * @Version: 1.0 + */ +@Slf4j +@Component +public class IPUtils { + + private static final String DB_PATH = "/ip2region.xdb"; + private static Searcher searcher; + + @PostConstruct + public void init() { + try { + // 从类路径加载资源文件 + InputStream inputStream = getClass().getResourceAsStream(DB_PATH); + if (inputStream == null) { + throw new FileNotFoundException("Resource not found: " + DB_PATH); + } + + // 将资源文件复制到临时文件 + Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); + Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); + + // 使用临时文件初始化 Searcher 对象 + searcher = Searcher.newWithFileOnly(tempDbPath.toString()); + } catch (Exception e) { + log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); + } + } + + /** + * 获取IP地址 + * + * @param request HttpServletRequest对象 + * @return 客户端IP地址 + */ + public static String getIpAddr(HttpServletRequest request) { + String ip = null; + try { + if (request == null) { + return ""; + } + ip = request.getHeader("x-forwarded-for"); + if (checkIp(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_CLIENT_IP"); + } + if (checkIp(ip)) { + ip = request.getHeader("HTTP_X_FORWARDED_FOR"); + } + if (checkIp(ip)) { + ip = request.getRemoteAddr(); + if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { + // 根据网卡取本机配置的IP + ip = getLocalAddr(); + } + } + } catch (Exception e) { + log.error("IPUtils ERROR, {}", e.getMessage()); + } + + // 使用代理,则获取第一个IP地址 + if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { + ip = ip.substring(0, ip.indexOf(",")); + } + + return ip; + } + + private static boolean checkIp(String ip) { + String unknown = "unknown"; + return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); + } + + /** + * 获取本机的IP地址 + * + * @return 本机IP地址 + */ + private static String getLocalAddr() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); + } + return null; + } + + /** + * 根据IP地址获取地理位置信息 + * + * @param ip IP地址 + * @return 地理位置信息 + */ + public static String getRegion(String ip) { + if (searcher == null) { + log.error("Searcher is not initialized"); + return null; + } + + try { + return searcher.search(ip); + } catch (Exception e) { + log.error("IpRegionUtil ERROR, {}", e.getMessage()); + return null; + } + } +} diff --git a/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..6c20b8ec4d9569e7fbd42a8ce7cec718f5324a4e --- /dev/null +++ b/hypersense-common/hypersense-common-core/src/main/java/tech/hypersense/common/core/utils/MapstructUtils.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.core.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import io.github.linpeilie.Converter; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Mapstruct 工具类 + *
参考文档:mapstruct-plus
映射 application.yml 中 security 前缀的安全相关配置
示例值:/api/v1/auth/login/**, /ws/** + */ + @NotEmpty + private String[] ignoreUrls; + + /** + * 非安全端点路径(允许匿名访问的API) + *
示例值:/doc.html, /v3/api-docs/** + */ + @NotEmpty + private String[] unsecuredUrls; + + /** + * 会话配置嵌套类 + */ + @Data + public static class SessionConfig { + /** + * 认证策略类型 + *
默认值:3600(1小时)
-1 表示永不过期
默认值:604800(7天)
HS256算法要求至少32个字符
示例:SecretKey012345678901234567890123456789
true - 允许同一账户多设备登录(默认)
false - 新登录会使旧令牌失效
定义用户可访问的数据范围,如全部、本部门或自定义范围
+ * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 + * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 + * @Version: 1.0 + */ +@Data +@NoArgsConstructor +public class SysUserDetails implements UserDetails { + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + /** + * 账号是否启用(true:启用 false:禁用) + */ + private Boolean enabled; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 数据权限范围 + */ + private Integer dataScope; + + /** + * 用户角色权限集合 + */ + private Collection authorities; + + /** + * 构造函数:根据用户认证信息初始化用户详情对象 + * + * @param user 用户认证信息对象 {@link UserAuthInfo} + */ + public SysUserDetails(UserAuthInfo user) { + this.userId = user.getUserId(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = ObjectUtil.equal(user.getStatus(), 1); + this.deptId = user.getDeptId(); + this.dataScope = user.getDataScope(); + + // 初始化角色权限集合 + this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) + ? user.getRoles().stream() + // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (user:add) + .map(role -> new SimpleGrantedAuthority(SecurityConstants.ROLE_PREFIX + role)) + .collect(Collectors.toSet()) + : Collections.emptySet(); + } + + + @Override + public Collection extends GrantedAuthority> getAuthorities() { + return this.authorities; + } + + @Override + public String getPassword() { + return this.password; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } +} + diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java new file mode 100644 index 0000000000000000000000000000000000000000..c3eee8cdc7b6f0f4955a25c2eaaf6754dd460a2c --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/service/PermissionService.java @@ -0,0 +1,97 @@ +package tech.hypersense.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.PatternMatchUtils; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; + +import java.util.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: SpringSecurity 权限校验 + * @Version: 1.0 + */ +@Component("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限:{}",requiredPerm); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(RedisConstants.System.ROLE_PERMS, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a8187f0f6513f0df8802fe17a6a4bb95d0efa2d5 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/TokenManager.java @@ -0,0 +1,62 @@ +package tech.hypersense.common.security.token; + +import org.springframework.security.core.Authentication; +import tech.hypersense.common.security.model.AuthenticationToken; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Token 管理器 + * + * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 用于生成、解析、校验、刷新 Token + * @Version: 1.0 + */ + +public interface TokenManager { + + /** + * 生成认证 Token + * + * @param authentication 用户认证信息 + * @return 认证 Token 响应 + */ + AuthenticationToken generateToken(Authentication authentication); + + /** + * 解析 Token 获取认证信息 + * + * @param token Token + * @return 用户认证信息 + */ + Authentication parseToken(String token); + + + /** + * 校验 Token 是否有效 + * + * @param token JWT Token + * @return 是否有效 + */ + boolean validateToken(String token); + + + /** + * 刷新 Token + * + * @param token 刷新令牌 + * @return 认证 Token 响应 + */ + AuthenticationToken refreshToken(String token); + + /** + * 将 Token 加入黑名单 + * + * @param token JWT Token + */ + default void blacklistToken(String token) { + // 默认实现可以是空的,或者抛出不支持的操作异常 + // throw new UnsupportedOperationException("Not implemented"); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3c346268d71ba11233014954452d6904381ddd92 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/JwtTokenManager.java @@ -0,0 +1,225 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.jwt.JWT; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.JwtClaimConstants; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WT Token 管理器 + *
+ * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt") +@Service +public class JwtTokenManager implements TokenManager { + + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + private final byte[] secretKey; + + public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes(); + } + + /** + * 生成令牌 + * + * @param authentication 认证信息 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken generateToken(Authentication authentication) { + int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); + + String accessToken = generateToken(authentication, accessTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenTimeToLive) + .build(); + } + + /** + * 解析令牌 + * + * @param token JWT Token + * @return Authentication 对象 + */ + @Override + public Authentication parseToken(String token) { + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID + userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID + userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + + userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 + // 角色集合 + Set authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES) + .stream() + .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) + .collect(Collectors.toSet()); + + return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @return 是否有效 + */ + @Override + public boolean validateToken(String token) { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + + // 判断是否在黑名单中,如果在,则返回false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) { + return false; + } + } + return isValid; + } + + /** + * 将令牌加入黑名单 + * + * @param token JWT Token + */ + @Override + public void blacklistToken(String token) { + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + } + + JWT jwt = JWTUtil.parseToken(token); + JSONObject payloads = jwt.getPayloads(); + + Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); + + // 黑名单Token Key + String blacklistTokenKey = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID); + + if (expirationAt != null) { + int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000); + if (expirationAt < currentTimeSeconds) { + // Token已过期,直接返回 + return; + } + // 计算Token剩余时间,将其加入黑名单 + int expirationIn = expirationAt - currentTimeSeconds; + redisTemplate.opsForValue().set(blacklistTokenKey, null, expirationIn, TimeUnit.SECONDS); + } else { + // 永不过期的Token永久加入黑名单 + redisTemplate.opsForValue().set(blacklistTokenKey, null); + } + ; + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 令牌响应对象 + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + + boolean isValid = validateToken(refreshToken); + if (!isValid) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + Authentication authentication = parseToken(refreshToken); + int accessTokenExpiration = securityProperties.getSession().getRefreshTokenTimeToLive(); + String newAccessToken = generateToken(authentication, accessTokenExpiration); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(accessTokenExpiration) + .build(); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl) { + + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + + Map payload = new HashMap<>(); + payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID + payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID + payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + + // claims 中添加角色信息 + Set roles = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + payload.put(JwtClaimConstants.AUTHORITIES, roles); + + Date now = new Date(); + payload.put(JWTPayload.ISSUED_AT, now); + + // 设置过期时间 -1 表示永不过期 + if (ttl != -1) { + Date expiresAt = DateUtil.offsetSecond(now, ttl); + payload.put(JWTPayload.EXPIRES_AT, expiresAt); + } + payload.put(JWTPayload.SUBJECT, authentication.getName()); + payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); + + return JWTUtil.createToken(payload, secretKey); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java new file mode 100644 index 0000000000000000000000000000000000000000..3cd385e843eb8bff54e048f60d57f45a438b0508 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/token/impl/RedisTokenManager.java @@ -0,0 +1,230 @@ +package tech.hypersense.common.security.token.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.config.properties.SecurityProperties; +import tech.hypersense.common.security.model.AuthenticationToken; +import tech.hypersense.common.security.model.OnlineUser; +import tech.hypersense.common.security.model.SysUserDetails; +import tech.hypersense.common.security.token.TokenManager; + +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Redis Token 管理器 + * + * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 用于生成、解析、校验、刷新 JWT Token + * @Version: 1.0 + */ +@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token") +@Service +public class RedisTokenManager implements TokenManager { + + // 安全配置属性 + private final SecurityProperties securityProperties; + private final RedisTemplate redisTemplate; + + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate redisTemplate) { + this.securityProperties = securityProperties; + this.redisTemplate = redisTemplate; + } + + @Override + public AuthenticationToken generateToken(Authentication authentication) { + SysUserDetails user = (SysUserDetails) authentication.getPrincipal(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + int refreshTtl = securityProperties.getSession().getRefreshTokenTimeToLive(); + + // 生成随机令牌 + String accessToken = IdUtil.fastSimpleUUID(); + String refreshToken = IdUtil.fastSimpleUUID(); + + // 构建用户在线信息(不包含密码) + OnlineUser onlineUser = buildOnlineUser(user); + + // 将访问令牌与刷新令牌与用户信息分别存入 Redis,并设置过期时间 + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, accessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken), + onlineUser, + refreshTtl, + TimeUnit.SECONDS + ); + + // 单设备登录控制,若不允许多设备登录,则通过用户ID映射保存当前最新的访问令牌 + Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin(); + if (!allowMultiLogin) { + Long userId = user.getUserId(); + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId); + // 获取当前用户已有的访问令牌 + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌对应的用户信息缓存 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + // 更新用户与访问令牌的映射 + redisTemplate.opsForValue().set(userAccessKey, accessToken, accessTtl, TimeUnit.SECONDS); + } + // 同时存储用户与刷新令牌的映射,便于后续刷新和踢出旧会话 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, user.getUserId()); + redisTemplate.opsForValue().set(userRefreshKey, refreshToken, refreshTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + /** + * 根据 token 解析用户信息 + * + * @param token JWT Token + * @return + */ + @Override + public Authentication parseToken(String token) { + // 根据访问令牌从 Redis 中获取在线用户信息 + String tokenUserCacheKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(tokenUserCacheKey); + + if (onlineUser == null) return null; + + // 构建用户权限集合 + Set authorities = onlineUser.getAuthorities().stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + + // 构建用户详情对象 + SysUserDetails userDetails = new SysUserDetails(); + userDetails.setUserId(onlineUser.getUserId()); + userDetails.setUsername(onlineUser.getUsername()); + userDetails.setDeptId(onlineUser.getDeptId()); + userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setAuthorities(authorities); + + return new UsernamePasswordAuthenticationToken(userDetails, null, authorities); + } + + /** + * 校验 Token 是否有效 + * + * @param token 访问令牌 + * @return + */ + @Override + public boolean validateToken(String token) { + String tokenKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + return redisTemplate.hasKey(tokenKey); + } + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return + */ + @Override + public AuthenticationToken refreshToken(String refreshToken) { + // 根据刷新令牌获取在线用户信息 + String refreshKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(refreshKey); + + if (onlineUser == null) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); + } + + // 获取当前用户的旧访问令牌(如果存在) + String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, onlineUser.getUserId()); + String oldAccessToken = (String) redisTemplate.opsForValue().get(userAccessKey); + if (oldAccessToken != null) { + // 删除旧的访问令牌记录 + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, oldAccessToken)); + } + + // 生成新访问令牌 + String newAccessToken = IdUtil.fastSimpleUUID(); + int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive(); + redisTemplate.opsForValue().set( + StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), + onlineUser, + accessTtl, + TimeUnit.SECONDS + ); + + // 更新用户与访问令牌的映射(若单设备登录,则更新映射以踢出旧会话) + redisTemplate.opsForValue().set(userAccessKey, newAccessToken, accessTtl, TimeUnit.SECONDS); + + return AuthenticationToken.builder() + .accessToken(newAccessToken) + .refreshToken(refreshToken) + .expiresIn(accessTtl) + .build(); + } + + + /** + * 将 Token 加入黑名单 + * + * @param token 访问令牌 + */ + @Override + public void blacklistToken(String token) { + // 删除访问令牌对应的在线用户信息缓存 + String accessKey = StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token); + OnlineUser onlineUser = (OnlineUser) redisTemplate.opsForValue().get(accessKey); + + if (onlineUser != null) { + Long userId = onlineUser.getUserId(); + + // 删除访问令牌缓存和用户与访问令牌的映射 + redisTemplate.delete(accessKey); + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId)); + + // 删除用户与刷新令牌的映射,以及刷新令牌对应的缓存 + String userRefreshKey = StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, userId); + String refreshToken = (String) redisTemplate.opsForValue().get(userRefreshKey); + if (refreshToken != null) { + redisTemplate.delete(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); + redisTemplate.delete(userRefreshKey); + } + } + } + + /** + * 构建 OnlineUser 对象 + */ + private OnlineUser buildOnlineUser(SysUserDetails user) { + Long userId = user.getUserId(); + Set roles = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + return new OnlineUser( + userId, + user.getUsername(), + user.getDeptId(), + user.getDataScope(), + roles + ); + } +} diff --git a/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d2e8aa3dcaca03227a93b652a6c827095fd78d --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/java/tech/hypersense/common/security/utils/SecurityUtils.java @@ -0,0 +1,124 @@ +package tech.hypersense.common.security.utils; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.security.model.SysUserDetails; + +import java.util.Collection; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Spring Security 工具类 + * @Version: 1.0 + */ +public class SecurityUtils { + + /** + * 获取当前登录人信息 + * + * @return Optional + */ + public static Optional getUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails) { + return Optional.of((SysUserDetails) principal); + } + } + return Optional.empty(); + } + + + /** + * 获取用户ID + * + * @return Long + */ + public static Long getUserId() { + return getUser().map(SysUserDetails::getUserId).orElse(null); + } + + + /** + * 获取用户账号 + * + * @return String 用户账号 + */ + public static String getUsername() { + return getUser().map(SysUserDetails::getUsername).orElse(null); + } + + + /** + * 获取部门ID + * + * @return Long + */ + public static Long getDeptId() { + return getUser().map(SysUserDetails::getDeptId).orElse(null); + } + + /** + * 获取数据权限范围 + * + * @return Integer + */ + public static Integer getDataScope() { + return getUser().map(SysUserDetails::getDataScope).orElse(null); + } + + + /** + * 获取角色集合 + * + * @return 角色集合 + */ + public static Set getRoles() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .map(Authentication::getAuthorities) + .filter(CollectionUtil::isNotEmpty) + .stream() + .flatMap(Collection::stream) + .map(GrantedAuthority::getAuthority) + // 筛选角色,authorities 中的角色都是以 ROLE_ 开头 + .filter(authority -> authority.startsWith(SecurityConstants.ROLE_PREFIX)) + .map(authority -> StrUtil.removePrefix(authority, SecurityConstants.ROLE_PREFIX)) + .collect(Collectors.toSet()); + } + + /** + * 是否超级管理员 + * + * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 超级管理员忽视任何权限判断 + */ + public static boolean isRoot() { + Set roles = getRoles(); + return roles.contains(SystemConstants.ROOT_ROLE_CODE); + } + + /** + * 获取请求中的 Token + * + * @return Token 字符串 + */ + public static String getTokenFromRequest() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } + + +} diff --git a/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e3fc6629a59b145f521c1a6cc5051c76a1f9d621 --- /dev/null +++ b/hypersense-common/hypersense-common-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,7 @@ +tech.hypersense.common.security.config.PasswordEncoderConfig +tech.hypersense.common.security.config.properties.SecurityProperties +tech.hypersense.common.security.service.PermissionService +tech.hypersense.common.security.token.impl.JwtTokenManager +tech.hypersense.common.security.token.impl.RedisTokenManager + + diff --git a/hypersense-common/hypersense-common-web/pom.xml b/hypersense-common/hypersense-common-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9508ea99ce5f11969f29e88af1cc94a014893762 --- /dev/null +++ b/hypersense-common/hypersense-common-web/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-web + + hypersense-common-web web服务模块 + + + + + tech.hypersense + hypersense-common-core + ${revision} + + + tech.hypersense + hypersense-shared-redis + + + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java new file mode 100644 index 0000000000000000000000000000000000000000..2241ecec84708b5b3046130179b6ce08701ea7c8 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/annotation/Debounce.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.annotation; + +import java.lang.annotation.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防止手抖导致重复提交注解 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + * + * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 + * @Version: 1.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Debounce { + + /** + * 锁过期时间(秒) + *
+ * 默认5秒内不允许重复提交 + */ + int expire() default 5; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..6e0864fc52614246419cb38517c2168a2636765f --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/apsect/DebounceAspect.java @@ -0,0 +1,100 @@ +package tech.hypersense.common.web.apsect; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.core.utils.IPUtils; +import tech.hypersense.common.web.annotation.Debounce; + +import java.util.concurrent.TimeUnit; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 防抖切面 + * @Version: 1.0 + */ + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class DebounceAspect { + + private final RedissonClient redissonClient; + + /** + * 防重复提交切点 + */ + @Pointcut("@annotation(debounce)") + public void repeatSubmitPointCut(Debounce debounce) { + } + + /** + * 环绕通知:处理防重复提交逻辑 + */ + @Around(value = "repeatSubmitPointCut(debounce)", argNames = "pjp,debounce") + public Object handleRepeatSubmit(ProceedingJoinPoint pjp, Debounce debounce) throws Throwable { + String lockKey = buildLockKey(); + + int expire = debounce.expire(); + RLock lock = redissonClient.getLock(lockKey); + + boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); + if (!locked) { + throw new BusinessException(ResultCode.USER_DUPLICATE_REQUEST); + } + return pjp.proceed(); + } + + /** + * 生成防重复提交锁的 key + * @return 锁的 key + */ + private String buildLockKey() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + // 用户唯一标识 + String userIdentifier = getUserIdentifier(request); + // 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法) + String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); + return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); + } + + /** + * 获取用户唯一标识 + * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 + * 2. 如果 Token 为空,使用 IP 作为用户唯一标识 + * + * @param request 请求对象 + * @return 用户唯一标识 + */ + private String getUserIdentifier(HttpServletRequest request) { + // 用户身份唯一标识 + String userIdentifier; + // 从请求头中获取 Token + String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token + userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 + } else { + userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 + } + return userIdentifier; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..0138d4e8e7bbdc93d052f07293ecfe0398158d83 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CaptchaConfig.java @@ -0,0 +1,55 @@ +package tech.hypersense.common.web.config; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.captcha.generator.MathGenerator; +import cn.hutool.captcha.generator.RandomGenerator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.hypersense.common.web.config.properties.CaptchaProperties; + +import java.awt.*; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码自动装配配置 + * @Version: 1.0 + */ +@Configuration +public class CaptchaConfig { + + @Autowired + private CaptchaProperties captchaProperties; + + /** + * 验证码文字生成器 + * + * @return CodeGenerator + */ + @Bean + public CodeGenerator codeGenerator() { + String codeType = captchaProperties.getCode().getType(); + int codeLength = captchaProperties.getCode().getLength(); + if ("math".equalsIgnoreCase(codeType)) { + return new MathGenerator(codeLength); + } else if ("random".equalsIgnoreCase(codeType)) { + return new RandomGenerator(codeLength); + } else { + throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); + } + } + + /** + * 验证码字体 + */ + @Bean + public Font captchaFont() { + String fontName = captchaProperties.getFont().getName(); + int fontSize = captchaProperties.getFont().getSize(); + int fontWight = captchaProperties.getFont().getWeight(); + return new Font(fontName, fontWight, fontSize); + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..2ef7313d372dd7316b3afb4c98a95437823bc243 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/CorsConfig.java @@ -0,0 +1,42 @@ +package tech.hypersense.common.web.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import java.util.Collections; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: CORS 资源共享配置 + * @Version: 1.0 + */ +@Configuration +public class CorsConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + CorsConfiguration corsConfiguration = new CorsConfiguration(); + //1.允许任何来源 + corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*")); + //2.允许任何请求头 + corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); + //3.允许任何方法 + corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); + //4.允许凭证 + corsConfiguration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", corsConfiguration); + CorsFilter corsFilter = new CorsFilter(source); + + FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean<>(corsFilter); + filterRegistrationBean.setOrder(-101); // 小于 SpringSecurity Filter的 Order(-100) 即可 + + return filterRegistrationBean; + } +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..903089984694447e86cf0f5e12d212aa51647282 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/WebMvcConfig.java @@ -0,0 +1,94 @@ +package tech.hypersense.common.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.TimeZone; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: Web 配置 + * @Version: 1.0 + */ +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 配置消息转换器 + * + * @param converters 消息转换器列表 + */ + @Override + public void configureMessageConverters(List> converters) { + MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + ObjectMapper objectMapper = new ObjectMapper(); + + // 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer) + JavaTimeModule javaTimeModule = new JavaTimeModule(); + // 返回指定字符串格式 + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER)); + // 反序列化,接受前端传来的格式 + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER)); + objectMapper.registerModule(javaTimeModule); + + // 配置全局日期格式和时区 + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8")); + + // 处理 Long/BigInteger 的精度问题 + SimpleModule simpleModule = new SimpleModule(); + simpleModule.addSerializer(Long.class, ToStringSerializer.instance); + simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance); + objectMapper.registerModule(simpleModule); + + jackson2HttpMessageConverter.setObjectMapper(objectMapper); + converters.add(1, jackson2HttpMessageConverter); + } + + /** + * 配置校验器 + * + * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory + * @return Validator 实例 + */ + @Bean + public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { + try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数 + .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) + .buildValidatorFactory()) { + + // 使用 try-with-resources 确保 ValidatorFactory 被正确关闭 + return validatorFactory.getValidator(); + } + } +} + diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..5358959eead53b685fe1b0ca2b398f29cfb88576 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/config/properties/CaptchaProperties.java @@ -0,0 +1,92 @@ +package tech.hypersense.common.web.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码 属性配置 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "captcha") +@Data +public class CaptchaProperties { + + /** + * 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 + */ + private String type; + + /** + * 验证码图片宽度 + */ + private int width; + /** + * 验证码图片高度 + */ + private int height; + + /** + * 干扰线数量 + */ + private int interfereCount; + + /** + * 文本透明度 + */ + private Float textAlpha; + + /** + * 验证码过期时间,单位:秒 + */ + private Long expireSeconds; + + /** + * 验证码字符配置 + */ + private CodeProperties code; + + /** + * 验证码字体 + */ + private FontProperties font; + + /** + * 验证码字符配置 + */ + @Data + public static class CodeProperties { + /** + * 验证码字符类型 math-算术|random-随机字符串 + */ + private String type; + /** + * 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 + */ + private int length; + } + + /** + * 验证码字体配置 + */ + @Data + public static class FontProperties { + /** + * 字体名称 + */ + private String name; + /** + * 字体样式 0-普通|1-粗体|2-斜体 + */ + private int weight; + /** + * 字体大小 + */ + private int size; + } + + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..34ccd5ce8c30c0329b84e58074584963f47057e2 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/enums/CaptchaTypeEnum.java @@ -0,0 +1,27 @@ +package tech.hypersense.common.web.enums; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码类型枚举 + * @Version: 1.0 + */ +public enum CaptchaTypeEnum { + + /** + * 圆圈干扰验证码 + */ + CIRCLE, + /** + * GIF验证码 + */ + GIF, + /** + * 干扰线验证码 + */ + LINE, + /** + * 扭曲干扰验证码 + */ + SHEAR +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..dd6b248db9aef3fba4f9e484725dbdaaa6776879 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/filter/CaptchaValidationFilter.java @@ -0,0 +1,75 @@ +package tech.hypersense.common.web.filter; + +import cn.hutool.captcha.generator.CodeGenerator; +import cn.hutool.core.util.StrUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.filter.OncePerRequestFilter; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.enums.ResultCode; +import tech.hypersense.common.core.utils.ResponseUtils; + +import java.io.IOException; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 图形验证码校验过滤器 + * @Version: 1.0 + */ +public class CaptchaValidationFilter extends OncePerRequestFilter { + + private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + + public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; + public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + + private final RedisTemplate redisTemplate; + + private final CodeGenerator codeGenerator; + + public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { + this.redisTemplate = redisTemplate; + this.codeGenerator = codeGenerator; + } + + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + // 检验登录接口的验证码 + if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { + // 请求中的验证码 + String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); + // TODO 兼容没有验证码的版本(线上请移除这个判断) + if (StrUtil.isBlank(captchaCode)) { + chain.doFilter(request, response); + return; + } + // 缓存中的验证码 + String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) + ); + if (cacheVerifyCode == null) { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + } else { + // 验证码比对 + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + chain.doFilter(request, response); + } else { + ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + } + } + } else { + // 非登录接口放行 + chain.doFilter(request, response); + } + } + +} diff --git a/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..967148dcbf49821d1343087af7a333e658ecce6a --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/java/tech/hypersense/common/web/model/CaptchaInfo.java @@ -0,0 +1,24 @@ +package tech.hypersense.common.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 验证码信息 + * @Version: 1.0 + */ +@Schema(description = "验证码信息") +@Data +@Builder +public class CaptchaInfo { + + @Schema(description = "验证码缓存 Key") + private String captchaKey; + + @Schema(description = "验证码图片Base64字符串") + private String captchaBase64; + +} diff --git a/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..1393dc45407109c6f2ec0fcba58ca65771ef2dc4 --- /dev/null +++ b/hypersense-common/hypersense-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,8 @@ +tech.hypersense.common.web.config.properties.CaptchaProperties +tech.hypersense.common.web.config.CaptchaConfig +tech.hypersense.common.web.config.CorsConfig +tech.hypersense.common.web.config.WebMvcConfig +tech.hypersense.common.web.filter.CaptchaValidationFilter +tech.hypersense.common.web.apsect.DebounceAspect + + diff --git a/hypersense-common/hypersense-common-websocket/pom.xml b/hypersense-common/hypersense-common-websocket/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3404db58b71591a215baf0bba0f6c5ec0830 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + tech.hypersense + hypersense-common + ${revision} + + hypersense-common-websocket + + + hypersense-common-websocket websocket服务模块 + + + + org.springframework.boot + spring-boot-starter-websocket + + + tech.hypersense + hypersense-common-core + + + diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..074f0450b2dce646e9c03385de6bf84ae16cfc4b --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/config/WebSocketConfig.java @@ -0,0 +1,107 @@ +package tech.hypersense.common.websocket.config; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.jwt.JWTPayload; +import cn.hutool.jwt.JWTUtil; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import tech.hypersense.common.core.constant.SecurityConstants; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 自动配置类 + * @Version: 1.0 + */ +// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递 +@EnableWebSocketMessageBroker +@Configuration +@Slf4j +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final ApplicationEventPublisher eventPublisher; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + /** + * 注册一个端点,客户端通过这个端点进行连接 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + // 注册 /ws 的端点 + .addEndpoint("/ws") + // 允许跨域 + .setAllowedOriginPatterns("*"); + } + + + /** + * 配置消息代理 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + // 客户端发送消息的请求前缀 + registry.setApplicationDestinationPrefixes("/app"); + + // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 + registry.enableSimpleBroker("/topic", "/queue"); + + // 服务端通知客户端的前缀,可以不设置,默认为user + registry.setUserDestinationPrefix("/user"); + } + + + /** + * 配置客户端入站通道拦截器 + * + * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + * + * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + * + * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult
+ * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 + * + * @param registration 通道注册器 + */ + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { + StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (accessor != null) { + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { + bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); + String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); + if (StrUtil.isNotBlank(username)) { + accessor.setUser(() -> username); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + } + } + } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + if (accessor.getUser() != null) { + String username = accessor.getUser().getName(); + eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); + } + } + } + return ChannelInterceptor.super.preSend(message, channel); + } + }); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3f20a8504ad6a25f92eaabfea634ef5693cb15 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/controller/WebsocketController.java @@ -0,0 +1,64 @@ +package tech.hypersense.common.websocket.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.websocket.model.ChatMessage; + +import java.security.Principal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: WebSocket 测试用例控制层 + *
+ * 包含点对点/广播发送消息 + * @Version: 1.0 + */ +@RestController +@RequestMapping("/websocket") +@RequiredArgsConstructor +@Slf4j +public class WebsocketController { + + private final SimpMessagingTemplate messagingTemplate; + + + /** + * 广播发送消息 + * + * @param message 消息内容 + */ + @MessageMapping("/sendToAll") + @SendTo("/topic/notice") + public String sendToAll(String message) { + return "服务端通知: " + message; + } + + /** + * 点对点发送消息 + *
+ * 模拟 张三 给 李四 发送消息场景 + * + * @param principal 当前用户 + * @param username 接收消息的用户 + * @param message 消息内容 + */ + @MessageMapping("/sendToUser/{username}") + public void sendToUser(Principal principal, @DestinationVariable String username, String message) { + // 发送人 + String sender = principal.getName(); + // 接收人 + String receiver = username; + + log.info("发送人:{}; 接收人:{}", sender, receiver); + // 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting + messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message)); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e86a6060ce453b81148ad7a16b3a856f4844505 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/handler/OnlineUserJobHandler.java @@ -0,0 +1,32 @@ +package tech.hypersense.common.websocket.handler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户定时任务 + * @Version: 1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserJobHandler { + + private final OnlineUserService onlineUserService; + private final SimpMessagingTemplate messagingTemplate; + + // 每分钟统计一次在线用户数 + @Scheduled(cron = "0 * * * * ?") + public void execute() { + log.info("定时任务:统计在线用户数"); + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java new file mode 100644 index 0000000000000000000000000000000000000000..d56e05a543df3ed5396566fcd70b9f0fcd9ffec5 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/listener/OnlineUserListener.java @@ -0,0 +1,44 @@ +package tech.hypersense.common.websocket.listener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; +import tech.hypersense.common.core.event.moudules.system.UserConnectionEvent; +import tech.hypersense.common.websocket.service.OnlineUserService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户监听器 + * @Version: 1.0 + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class OnlineUserListener { + + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 用户连接事件处理 + * + * @param event 用户连接事件 + */ + @EventListener + public void handleUserConnectionEvent(UserConnectionEvent event) { + String username = event.getUsername(); + if (event.isConnected()) { + onlineUserService.addOnlineUser(username); + log.info("User connected: {}", username); + } else { + onlineUserService.removeOnlineUser(username); + log.info("User disconnected: {}", username); + } + // 推送在线用户人数 + messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + } + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad6edb20503d5a3adff24f092262aecefb67ff9 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/model/ChatMessage.java @@ -0,0 +1,28 @@ +package tech.hypersense.common.websocket.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统消息体 + * @Version: 1.0 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + + /** + * 发送者 + */ + private String sender; + + /** + * 消息内容 + */ + private String content; + +} \ No newline at end of file diff --git a/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..6827e36297b6039c1640fb043e1b1ae282b144ee --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/java/tech/hypersense/common/websocket/service/OnlineUserService.java @@ -0,0 +1,69 @@ +package tech.hypersense.common.websocket.service; + +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 在线用户服务 + * @Version: 1.0 + */ +@Service +public class OnlineUserService { + + private final Set onlineUsers = ConcurrentHashMap.newKeySet(); + + /** + * 添加用户到在线用户集合 + * + * @param username 用户名 + */ + public void addOnlineUser(String username) { + onlineUsers.add(username); + } + + /** + * 从在线用户集合移除用户 + * + * @param username 用户名 + */ + public void removeOnlineUser(String username) { + onlineUsers.remove(username); + } + + /** + * 获取所有在线用户 + * + * @return 在线用户集合 + */ + public Set getAllOnlineUsers() { + return Collections.unmodifiableSet(onlineUsers); + } + + /** + * 获取在线的接收者 + * 从所有接收者中过滤出在线的接收者 + * + * @param receivers 接收者 + * @return 在线的接收者集合 + */ + public Set getOnlineReceivers(Set receivers) { + return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数量 + */ + public int getOnlineUserCount() { + return onlineUsers.size(); + } + + +} diff --git a/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..25691e553ac18992b0beef7f0349195017bf2398 --- /dev/null +++ b/hypersense-common/hypersense-common-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,4 @@ +tech.hypersense.common.websocket.config.WebSocketConfig + + + diff --git a/hypersense-common/pom.xml b/hypersense-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..c1afc52ce50ece4f1d531bffaa726c3bb742549f --- /dev/null +++ b/hypersense-common/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + tech.hypersense + hypersense-template + ${revision} + ../pom.xml + + hypersense-common + pom + + hypersense-common-bom + hypersense-common-websocket + hypersense-common-web + hypersense-common-core + hypersense-common-security + hypersense-common-log + hypersense-common-mybatis + hypersense-common-doc + + + + hypersense-common 内部公共服务模块 + + diff --git a/hypersense-modules/hypersense-codegen/pom.xml b/hypersense-modules/hypersense-codegen/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..84994c64085d358406dae822a1da765d7e50fbab --- /dev/null +++ b/hypersense-modules/hypersense-codegen/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-codegen + + hypersense-codegen 代码生成业务模块 + + + + tech.hypersense + hypersense-common-core + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-system + + + + com.baomidou + mybatis-plus-generator + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3ef0e4b9945f066297de013ce64652c35ad0a0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/config/properties/CodegenProperties.java @@ -0,0 +1,96 @@ +package tech.hypersense.codegen.config.properties; + +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.map.MapUtil; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置属性 + * @Version: 1.0 + */ +@Component +@ConfigurationProperties(prefix = "codegen") +@Data +public class CodegenProperties { + + + /** + * 默认配置 + */ + private DefaultConfig defaultConfig ; + + /** + * 模板配置 + */ + private Map templateConfigs = MapUtil.newHashMap(true); + + /** + * 后端应用名 + */ + private String backendAppName; + + /** + * 前端应用名 + */ + private String frontendAppName; + + /** + * 下载文件名 + */ + private String downloadFileName; + + /** + * 排除数据表 + */ + private List excludeTables; + + /** + * 模板配置 + */ + @Data + public static class TemplateConfig { + + /** + * 模板路径 (e.g. /templates/codegen/controller.java.vm) + */ + private String templatePath; + + /** + * 子包名 (e.g. controller/service/mapper/model) + */ + private String subpackageName; + + /** + * 文件扩展名,如 .java + */ + private String extension = FileNameUtil.EXT_JAVA; + + } + + /** + * 默认配置 + */ + @Data + public static class DefaultConfig { + + /** + * 作者 (e.g. Ray) + */ + private String author; + + /** + * 默认模块名(e.g. system) + */ + private String moduleName; + + } + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cfe21d28ef598acb1d482134c11cb58672ca0ec --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/controller/CodegenController.java @@ -0,0 +1,107 @@ +package tech.hypersense.codegen.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.common.log.annotation.Log; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 代码生成器控制层 + * @Version: 1.0 + */ +@Tag(name = "11.代码生成") +@RestController +@RequestMapping("/api/v1/codegen") +@RequiredArgsConstructor +@Slf4j +public class CodegenController { + + private final CodegenService codegenService; + private final GenConfigService genConfigService; + private final CodegenProperties codegenProperties; + @Operation(summary = "获取数据表分页列表") + @GetMapping("/table/page") + @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER) + public PageResult getTablePage( + TablePageQuery queryParams + ) { + Page result = codegenService.getTablePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取代码生成配置") + @GetMapping("/{tableName}/config") + public Result getGenConfigFormData( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + return Result.success(formData); + } + + @Operation(summary = "保存代码生成配置") + @PostMapping("/{tableName}/config") + @Log(value = "生成代码", module = LogModuleEnum.OTHER) + public Result> saveGenConfig(@RequestBody GenConfigForm formData) { + genConfigService.saveGenConfig(formData); + return Result.success(); + } + + @Operation(summary = "删除代码生成配置") + @DeleteMapping("/{tableName}/config") + public Result> deleteGenConfig( + @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName + ) { + genConfigService.deleteGenConfig(tableName); + return Result.success(); + } + + @Operation(summary = "获取预览生成代码") + @GetMapping("/{tableName}/preview") + @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) + public Result> getTablePreviewData(@PathVariable String tableName) { + List list = codegenService.getCodegenPreviewData(tableName); + return Result.success(list); + } + + @Operation(summary = "下载代码") + @GetMapping("/{tableName}/download") + @Log(value = "下载代码", module = LogModuleEnum.OTHER) + public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + String[] tableNames = tableName.split(","); + byte[] data = codegenService.downloadCode(tableNames); + + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); + response.setContentType("application/octet-stream; charset=UTF-8"); + + try (ServletOutputStream outputStream = response.getOutputStream()) { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + log.error("Error while writing the zip file to response", e); + throw new RuntimeException("Failed to write the zip file to response", e); + } + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..f4993825864525429eaf14c7336b44588d62a4e0 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/converter/CodegenConverter.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface CodegenConverter { + + @Mapping(source = "genConfig.tableName", target = "tableName") + @Mapping(source = "genConfig.businessName", target = "businessName") + @Mapping(source = "genConfig.moduleName", target = "moduleName") + @Mapping(source = "genConfig.packageName", target = "packageName") + @Mapping(source = "genConfig.entityName", target = "entityName") + @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "fieldConfigs", target = "fieldConfigs") + GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + + List toGenFieldConfigForm(List fieldConfigs); + + GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + + GenConfig toGenConfig(GenConfigForm formData); + + List toGenFieldConfig(List fieldConfigs); + + GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..3c3cf0dbefaf829a4d52b0fe0bde3e7cfd00cec1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/FormTypeEnum.java @@ -0,0 +1,89 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum FormTypeEnum implements IBaseEnum { + + /** + * 输入框 + */ + INPUT(1, "输入框"), + + /** + * 下拉框 + */ + SELECT(2, "下拉框"), + + /** + * 单选框 + */ + RADIO(3, "单选框"), + + /** + * 复选框 + */ + CHECK_BOX(4, "复选框"), + + /** + * 数字输入框 + */ + INPUT_NUMBER(5, "数字输入框"), + + /** + * 开关 + */ + SWITCH(6, "开关"), + + /** + * 文本域 + */ + TEXT_AREA(7, "文本域"), + + /** + * 日期时间框 + */ + DATE(8, "日期框"), + + /** + * 日期框 + */ + DATE_TIME(9, "日期时间框"), + + /** + * 隐藏域 + */ + HIDDEN(10, "隐藏域"); + + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + @JsonValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + + @JsonCreator + public static FormTypeEnum fromValue(Integer value) { + for (FormTypeEnum type : FormTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..dc4f729ab8ebc8a3049240a29c14a4b88da7c9f6 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/JavaTypeEnum.java @@ -0,0 +1,84 @@ +package tech.hypersense.codegen.enums; + +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 表单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum JavaTypeEnum { + + VARCHAR("varchar", "String", "string"), + CHAR("char", "String", "string"), + BLOB("blob", "byte[]", "Uint8Array"), + TEXT("text", "String", "string"), + JSON("json", "String", "any"), + INTEGER("int", "Integer", "number"), + TINYINT("tinyint", "Integer", "number"), + SMALLINT("smallint", "Integer", "number"), + MEDIUMINT("mediumint", "Integer", "number"), + BIGINT("bigint", "Long", "number"), + FLOAT("float", "Float", "number"), + DOUBLE("double", "Double", "number"), + DECIMAL("decimal", "BigDecimal", "number"), + DATE("date", "LocalDate", "Date"), + DATETIME("datetime", "LocalDateTime", "Date"); + + // 数据库类型 + private final String dbType; + // Java类型 + private final String javaType; + // TypeScript类型 + private final String tsType; + + // 数据库类型和Java类型的映射 + private static final Map typeMap = new HashMap<>(); + + // 初始化映射关系 + static { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); + } + } + + JavaTypeEnum(String dbType, String javaType, String tsType) { + this.dbType = dbType; + this.javaType = javaType; + this.tsType = tsType; + } + + /** + * 根据数据库类型获取对应的Java类型 + * + * @param columnType 列类型 + * @return 对应的Java类型 + */ + public static String getJavaTypeByColumnType(String columnType) { + JavaTypeEnum javaTypeEnum = typeMap.get(columnType); + if (javaTypeEnum != null) { + return javaTypeEnum.getJavaType(); + } + return null; + } + + /** + * 根据Java类型获取对应的TypeScript类型 + * + * @param javaType Java类型 + * @return 对应的TypeScript类型 + */ + public static String getTsTypeByJavaType(String javaType) { + for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { + if (javaTypeEnum.getJavaType().equals(javaType)) { + return javaTypeEnum.getTsType(); + } + } + return null; + } +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..2120159ffbbc577f3549d1be98bce6d775b5cb11 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/enums/QueryTypeEnum.java @@ -0,0 +1,73 @@ +package tech.hypersense.codegen.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 查询类型枚举 + * @Version: 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum QueryTypeEnum implements IBaseEnum { + + /** 等于 */ + EQ(1, "="), + + /** 模糊匹配 */ + LIKE(2, "LIKE '%s%'"), + + /** 包含 */ + IN(3, "IN"), + + /** 范围 */ + BETWEEN(4, "BETWEEN"), + + /** 大于 */ + GT(5, ">"), + + /** 大于等于 */ + GE(6, ">="), + + /** 小于 */ + LT(7, "<"), + + /** 小于等于 */ + LE(8, "<="), + + /** 不等于 */ + NE(9, "!="), + + /** 左模糊匹配 */ + LIKE_LEFT(10, "LIKE '%s'"), + + /** 右模糊匹配 */ + LIKE_RIGHT(11, "LIKE 's%'"); + + + // 存储在数据库中的枚举属性值 + @EnumValue + @JsonValue + private final Integer value; + + // 序列化成 JSON 时的属性值 + private final String label; + + + @JsonCreator + public static QueryTypeEnum fromValue(Integer value) { + for (QueryTypeEnum type : QueryTypeEnum.values()) { + if (type.getValue().equals(value)) { + return type; + } + } + throw new IllegalArgumentException("No enum constant with value " + value); + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b556c43c1ba8d4ea0fb7e9b76ea51ea32b442514 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/DatabaseMapper.java @@ -0,0 +1,46 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库映射层 + * @Version: 1.0 + */ +@Mapper +public interface DatabaseMapper extends BaseMapper { + + /** + * 获取表分页列表 + * + * @param page + * @param queryParams + * @return + */ + Page getTablePage(Page page, TablePageQuery queryParams); + + /** + * 获取表字段列表 + * + * @param tableName + * @return + */ + List getTableColumns(String tableName); + + /** + * 获取表元数据 + * + * @param tableName + * @return + */ + TableMetaData getTableMetadata(String tableName); +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..14f7c7488e82df5319bbdc768ca87c9ac0a02d98 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成基础配置访问层 + * @Version: 1.0 + */ +@Mapper +public interface GenConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a2c88c980a94ccefd1be93d39127599159407dfa --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/mapper/GenFieldConfigMapper.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置访问层 + * @Version: 1.0 + */ +public interface GenFieldConfigMapper extends BaseMapper { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..bfa56f93efba37a40928273da5fa0074f5cf8a2b --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/ColumnMetaData.java @@ -0,0 +1,57 @@ +package tech.hypersense.codegen.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表字段VO + * @Version: 1.0 + */ +@Schema(description = "数据表字段VO") +@Data +public class ColumnMetaData { + + /** + * 字段名称 + */ + private String columnName; + + /** + * 字段类型 + */ + private String dataType; + + /** + * 字段描述 + */ + private String columnComment; + + /** + * 字段长度 + */ + private Long characterMaximumLength; + + /** + * 是否主键(1-是 0-否) + */ + private Integer isPrimaryKey; + + /** + * 是否可为空(1-是 0-否) + */ + private String isNullable; + + /** + * 字符集 + */ + private String characterSetName; + + /** + * 排序规则 + */ + private String collationName; + +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java new file mode 100644 index 0000000000000000000000000000000000000000..2ccd151110e5b5cfdcd33a9fb7e44a8808a6bd28 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/bo/TableMetaData.java @@ -0,0 +1,44 @@ +package tech.hypersense.codegen.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表元数据 + * @Version: 1.0 + */ +@Data +public class TableMetaData { + + /** + * 表名称 + */ + private String tableName; + + /** + * 表描述 + */ + private String tableComment; + + /** + * 排序规则 + */ + private String tableCollation; + + /** + * 存储引擎 + */ + private String engine; + + /** + * 字符集 + */ + private String charset; + + /** + * 创建时间 + */ + private String createTime; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..027a647f492f8af9512d017dad7312b8cf5e4f19 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/entity/GenFieldConfig.java @@ -0,0 +1,105 @@ +package tech.hypersense.codegen.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字段生成配置实体 + * @Version: 1.0 + */ +@TableName(value = "gen_field_config") +@Getter +@Setter +public class GenFieldConfig extends BaseEntity { + + + /** + * 关联的配置ID + */ + private Long configId; + + /** + * 列名 + */ + private String columnName; + + /** + * 列类型 + */ + private String columnType; + + /** + * 字段长度 + */ + private Long maxLength; + + /** + * 字段名称 + */ + private String fieldName; + + /** + * 字段排序 + */ + private Integer fieldSort; + + /** + * 字段类型 + */ + private String fieldType; + + /** + * 字段描述 + */ + private String fieldComment; + + /** + * 表单类型 + */ + private FormTypeEnum formType; + + /** + * 查询方式 + */ + private QueryTypeEnum queryType; + + /** + * 是否在列表显示 + */ + private Integer isShowInList; + + /** + * 是否在表单显示 + */ + private Integer isShowInForm; + + /** + * 是否在查询条件显示 + */ + private Integer isShowInQuery; + + /** + * 是否必填 + */ + private Integer isRequired; + + /** + * TypeScript类型 + */ + @TableField(exist = false) + @JsonIgnore + private String tsType; + + /** + * 字典类型 + */ + private String dictType; +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..3119ab82755f47470e53ad5570ec1e5fdda40565 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/form/GenConfigForm.java @@ -0,0 +1,104 @@ +package tech.hypersense.codegen.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置表单 + * @Version: 1.0 + */ +@Schema(description = "代码生成配置表单") +@Data +public class GenConfigForm { + + @Schema(description = "主键",example = "1") + private Long id; + + @Schema(description = "表名",example = "sys_user") + private String tableName; + + @Schema(description = "业务名",example = "用户") + private String businessName; + + @Schema(description = "模块名",example = "system") + private String moduleName; + + @Schema(description = "包名",example = "com.youlai") + private String packageName; + + @Schema(description = "实体名",example = "User") + private String entityName; + + @Schema(description = "作者",example = "youlaitech") + private String author; + + @Schema(description = "上级菜单ID",example = "1") + private Long parentMenuId; + + @Schema(description = "字段配置列表") + private List fieldConfigs; + + @Schema(description = "后端应用名") + private String backendAppName; + + @Schema(description = "前端应用名") + private String frontendAppName; + + @Schema(description = "字段配置") + @Data + public static class FieldConfig { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "列名") + private String columnName; + + @Schema(description = "列类型") + private String columnType; + + @Schema(description = "字段名") + private String fieldName; + + @Schema(description = "字段排序") + private Integer fieldSort; + + @Schema(description = "字段类型") + private String fieldType; + + @Schema(description = "字段描述") + private String fieldComment; + + @Schema(description = "是否在列表显示") + private Integer isShowInList; + + @Schema(description = "是否在表单显示") + private Integer isShowInForm; + + @Schema(description = "是否在查询条件显示") + private Integer isShowInQuery; + + @Schema(description = "是否必填") + private Integer isRequired; + + @Schema(description = "最大长度") + private Integer maxLength; + + @Schema(description = "表单类型") + private FormTypeEnum formType; + + @Schema(description = "查询类型") + private QueryTypeEnum queryType; + + @Schema(description = "字典类型") + private String dictType; + + } +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..209abeb54af9c1507391bb274ac37edca9798c50 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/query/TablePageQuery.java @@ -0,0 +1,31 @@ +package tech.hypersense.codegen.model.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据表分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "数据表分页查询对象") +@Getter +@Setter +public class TablePageQuery extends BasePageQuery { + + @Schema(description="关键字(表名)") + private String keywords; + + /** + * 排除的表名 + */ + @JsonIgnore + private List excludeTables; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2f17b8a62010693b584e69daeef03cf2d8894846 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/CodegenPreviewVO.java @@ -0,0 +1,25 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成代码预览VO + * @Version: 1.0 + */ +@Schema(description = "代码生成代码预览VO") +@Data +public class CodegenPreviewVO { + + @Schema(description = "生成文件路径") + private String path; + + @Schema(description = "生成文件名称",example = "SysUser.java" ) + private String fileName; + + @Schema(description = "生成文件内容") + private String content; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d9b4f5f1e29aacbc272673cfef3ede95e2bf9de1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/model/vo/TablePageVO.java @@ -0,0 +1,37 @@ +package tech.hypersense.codegen.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "表视图对象") +@Data +public class TablePageVO { + + @Schema(description = "表名称", example = "sys_user") + private String tableName; + + @Schema(description = "表描述",example = "用户表") + private String tableComment; + + @Schema(description = "表排序规则",example = "utf8mb4_general_ci") + private String tableCollation; + + @Schema(description = "存储引擎",example = "InnoDB") + private String engine; + + @Schema(description = "字符集",example = "utf8mb4") + private String charset; + + @Schema(description = "创建时间",example = "2023-08-08 08:08:08") + private String createTime; + + @Schema(description="是否已配置") + private Integer isConfigured; + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java new file mode 100644 index 0000000000000000000000000000000000000000..a450729c2aaec1b2212c0b216a91dee36d5c78e8 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/CodegenService.java @@ -0,0 +1,41 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface CodegenService { + + /** + * 获取数据表分页列表 + * + * @param queryParams 查询参数 + * @return + */ + Page getTablePage(TablePageQuery queryParams); + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return + */ + List getCodegenPreviewData(String tableName); + + /** + * 下载代码 + * @param tableNames 表名 + * @return + */ + byte[] downloadCode(String[] tableNames); +} + diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..7088f1877fb47ed9356f8385801a8394241d5b6e --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenConfigService.java @@ -0,0 +1,39 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenConfigService extends IService { + + /** + * 获取代码生成配置 + * + * @param tableName 表名 + * @return + */ + GenConfigForm getGenConfigFormData(String tableName); + + /** + * 保存代码生成配置 + * + * @param formData 表单数据 + * @return + */ + void saveGenConfig(GenConfigForm formData); + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + * @return + */ + void deleteGenConfig(String tableName); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..9af9e1904fe3fa23fb05eaaeb0464629867855fc --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/GenFieldConfigService.java @@ -0,0 +1,14 @@ +package tech.hypersense.codegen.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.codegen.model.entity.GenFieldConfig; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成配置接口 + * @Version: 1.0 + */ +public interface GenFieldConfigService extends IService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a7a03ac832242ff57127d0b333bbbbd6e3388cb4 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/CodegenServiceImpl.java @@ -0,0 +1,316 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.Template; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.TemplateUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.query.TablePageQuery; +import tech.hypersense.codegen.model.vo.CodegenPreviewVO; +import tech.hypersense.codegen.model.vo.TablePageVO; +import tech.hypersense.codegen.service.CodegenService; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class CodegenServiceImpl implements CodegenService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenConfigService genConfigService; + private final GenFieldConfigService genFieldConfigService; + + /** + * 数据表分页列表 + * + * @param queryParams 查询参数 + * @return 分页结果 + */ + public Page getTablePage(TablePageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + // 设置排除的表 + List excludeTables = codegenProperties.getExcludeTables(); + queryParams.setExcludeTables(excludeTables); + + return databaseMapper.getTablePage(page, queryParams); + } + + /** + * 获取预览生成代码 + * + * @param tableName 表名 + * @return 预览数据 + */ + @Override + public List getCodegenPreviewData(String tableName) { + + List list = new ArrayList<>(); + + GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (genConfig == null) { + throw new BusinessException("未找到表生成配置"); + } + + List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + + ); + if (CollectionUtil.isEmpty(fieldConfigs)) { + throw new BusinessException("未找到字段生成配置"); + } + + // 遍历模板配置 + Map templateConfigs = codegenProperties.getTemplateConfigs(); + for (Map.Entry templateConfigEntry : templateConfigs.entrySet()) { + CodegenPreviewVO previewVO = new CodegenPreviewVO(); + + CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); + + /* 1. 生成文件名 UserController */ + // User Role Menu Dept + String entityName = genConfig.getEntityName(); + // Controller Service Mapper Entity + String templateName = templateConfigEntry.getKey(); + // .java .ts .vue + String extension = templateConfig.getExtension(); + + // 文件名 UserController.java + String fileName = getFileName(entityName, templateName, extension); + previewVO.setFileName(fileName); + + /* 2. 生成文件路径 */ + // 包名:com.youlai.boot + String packageName = genConfig.getPackageName(); + // 模块名:system + String moduleName = genConfig.getModuleName(); + // 子包名:controller + String subpackageName = templateConfig.getSubpackageName(); + // 组合成文件路径:src/main/java/com/youlai/boot/system/controller + String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); + previewVO.setPath(filePath); + + /* 3. 生成文件内容 */ + // 将模板文件中的变量替换为具体的值 生成代码内容 + String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + previewVO.setContent(content); + + list.add(previewVO); + } + return list; + } + + /** + * 生成文件名 + * + * @param entityName 实体类名 UserController + * @param templateName 模板名 Entity + * @param extension 文件后缀 .java + * @return 文件名 + */ + private String getFileName(String entityName, String templateName, String extension) { + if ("Entity".equals(templateName)) { + return entityName + extension; + } else if ("MapperXml".equals(templateName)) { + return entityName + "Mapper" + extension; + } else if ("API".equals(templateName)) { + return StrUtil.toSymbolCase(entityName, '-') + extension; + } else if ("VIEW".equals(templateName)) { + return "index.vue"; + } + return entityName + templateName + extension; + } + + /** + * 生成文件路径 + * + * @param templateName 模板名 Entity + * @param moduleName 模块名 system + * @param packageName 包名 com.youlai + * @param subPackageName 子包名 controller + * @param entityName 实体类名 UserController + * @return 文件路径 src/main/java/com/youlai/system/controller + */ + private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { + String path; + if ("MapperXml".equals(templateName)) { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "resources" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("API".equals(templateName)) { + // path = "src/api/system"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + ); + } else if ("VIEW".equals(templateName)) { + // path = "src/views/system/user"; + path = (codegenProperties.getFrontendAppName() + + File.separator + "src" + + File.separator + subPackageName + + File.separator + moduleName + + File.separator + StrUtil.toSymbolCase(entityName, '-') + ); + } else { + path = (codegenProperties.getBackendAppName() + + File.separator + + "src" + File.separator + "main" + File.separator + "java" + + File.separator + packageName + + File.separator + moduleName + + File.separator + subPackageName + ); + } + + // subPackageName = model.entity => model/entity + path = path.replace(".", File.separator); + + return path; + } + + /** + * 生成代码内容 + * + * @param templateConfig 模板配置 + * @param genConfig 生成配置 + * @param fieldConfigs 字段配置 + * @return 代码内容 + */ + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + + Map bindMap = new HashMap<>(); + + String entityName = genConfig.getEntityName(); + + bindMap.put("packageName", genConfig.getPackageName()); + bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("subpackageName", templateConfig.getSubpackageName()); + bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); + bindMap.put("entityName", entityName); + bindMap.put("tableName", genConfig.getTableName()); + bindMap.put("author", genConfig.getAuthor()); + bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest + bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket + bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("fieldConfigs", fieldConfigs); + + boolean hasLocalDateTime = false; + boolean hasBigDecimal = false; + boolean hasRequiredField = false; + + for (GenFieldConfig fieldConfig : fieldConfigs) { + + if ("LocalDateTime".equals(fieldConfig.getFieldType())) { + hasLocalDateTime = true; + } + if ("BigDecimal".equals(fieldConfig.getFieldType())) { + hasBigDecimal = true; + } + if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { + hasRequiredField = true; + } + fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); + } + + bindMap.put("hasLocalDateTime", hasLocalDateTime); + bindMap.put("hasBigDecimal", hasBigDecimal); + bindMap.put("hasRequiredField", hasRequiredField); + + TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); + Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + + return template.render(bindMap); + } + + /** + * 下载代码 + * + * @param tableNames 表名数组,支持多张表。 + * @return 压缩文件字节数组 + */ + @Override + public byte[] downloadCode(String[] tableNames) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(outputStream)) { + + // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 + for (String tableName : tableNames) { + generateAndZipCode(tableName, zip); + } + // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 + zip.finish(); + return outputStream.toByteArray(); + + } catch (IOException e) { + log.error("Error while generating zip for code download", e); + throw new RuntimeException("Failed to generate code zip file", e); + } + } + + /** + * 根据表名生成代码并压缩到zip文件中 + * + * @param tableName 表名 + * @param zip 压缩文件输出流 + */ + private void generateAndZipCode(String tableName, ZipOutputStream zip) { + List codePreviewList = getCodegenPreviewData(tableName); + + for (CodegenPreviewVO codePreview : codePreviewList) { + String fileName = codePreview.getFileName(); + String content = codePreview.getContent(); + String path = codePreview.getPath(); + + try { + // 创建压缩条目 + ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); + zip.putNextEntry(zipEntry); + + // 写入文件内容 + zip.write(content.getBytes(StandardCharsets.UTF_8)); + + // 关闭当前压缩条目 + zip.closeEntry(); + + } catch (IOException e) { + log.error("Error while adding file {} to zip", fileName, e); + } + } + } + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5bb8d9336825f8254eed9995fa76abd9df54aa53 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenConfigServiceImpl.java @@ -0,0 +1,220 @@ +package tech.hypersense.codegen.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.enums.EnvEnum; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.codegen.config.properties.CodegenProperties; +import tech.hypersense.codegen.converter.CodegenConverter; +import tech.hypersense.codegen.enums.FormTypeEnum; +import tech.hypersense.codegen.enums.JavaTypeEnum; +import tech.hypersense.codegen.enums.QueryTypeEnum; +import tech.hypersense.codegen.mapper.DatabaseMapper; +import tech.hypersense.codegen.mapper.GenConfigMapper; +import tech.hypersense.codegen.model.bo.ColumnMetaData; +import tech.hypersense.codegen.model.bo.TableMetaData; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.model.form.GenConfigForm; +import tech.hypersense.codegen.service.GenConfigService; +import tech.hypersense.codegen.service.GenFieldConfigService; +import tech.hypersense.system.service.MenuService; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 数据库服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { + + private final DatabaseMapper databaseMapper; + private final CodegenProperties codegenProperties; + private final GenFieldConfigService genFieldConfigService; + private final CodegenConverter codegenConverter; + + @Value("${spring.profiles.active}") + private String springProfilesActive; + + private final MenuService menuService; + + /** + * 获取代码生成配置 + * + * @param tableName 表名 eg: sys_user + * @return 代码生成配置 + */ + @Override + public GenConfigForm getGenConfigFormData(String tableName) { + // 查询表生成配置 + GenConfig genConfig = this.getOne( + new LambdaQueryWrapper<>(GenConfig.class) + .eq(GenConfig::getTableName, tableName) + .last("LIMIT 1") + ); + + // 是否有代码生成配置 + boolean hasGenConfig = genConfig != null; + + // 如果没有代码生成配置,则根据表的元数据生成默认配置 + if (genConfig == null) { + TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); + Assert.isTrue(tableMetadata != null, "未找到表元数据"); + + genConfig = new GenConfig(); + genConfig.setTableName(tableName); + + // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 + String tableComment = tableMetadata.getTableComment(); + if (StrUtil.isNotBlank(tableComment)) { + genConfig.setBusinessName(tableComment.replace("表", "").trim()); + } + // 根据表名生成实体类名 例如:sys_user -> SysUser + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + + genConfig.setPackageName("tech.hypersense"); + genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + } + + // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 + List genFieldConfigs = new ArrayList<>(); + + // 获取表的列 + List tableColumns = databaseMapper.getTableColumns(tableName); + if (CollectionUtil.isNotEmpty(tableColumns)) { + // 查询字段生成配置 + List fieldConfigList = genFieldConfigService.list( + new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + .orderByAsc(GenFieldConfig::getFieldSort) + ); + Integer maxSort = fieldConfigList.stream() + .map(GenFieldConfig::getFieldSort) + .filter(Objects::nonNull) // 过滤掉空值 + .max(Integer::compareTo) + .orElse(0); + for (ColumnMetaData tableColumn : tableColumns) { + // 根据列名获取字段生成配置 + String columnName = tableColumn.getColumnName(); + GenFieldConfig fieldConfig = fieldConfigList.stream() + .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) + .findFirst() + .orElseGet(() -> createDefaultFieldConfig(tableColumn)); + if (fieldConfig.getFieldSort() == null) { + fieldConfig.setFieldSort(++maxSort); + } + // 根据列类型设置字段类型 + String fieldType = fieldConfig.getFieldType(); + if (StrUtil.isBlank(fieldType)) { + String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); + fieldConfig.setFieldType(javaType); + } + // 如果没有代码生成配置,则默认展示在列表和表单 + if (!hasGenConfig) { + fieldConfig.setIsShowInList(1); + fieldConfig.setIsShowInForm(1); + } + genFieldConfigs.add(fieldConfig); + } + } + // 对 genFieldConfigs 按照 fieldSort 排序 + genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList(); + GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs); + + genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); + genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); + return genConfigForm; + } + + + /** + * 创建默认字段配置 + * + * @param columnMetaData 表字段元数据 + * @return + */ + private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) { + GenFieldConfig fieldConfig = new GenFieldConfig(); + fieldConfig.setColumnName(columnMetaData.getColumnName()); + fieldConfig.setColumnType(columnMetaData.getDataType()); + fieldConfig.setFieldComment(columnMetaData.getColumnComment()); + fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName())); + fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1); + + if (fieldConfig.getColumnType().equals("date")) { + fieldConfig.setFormType(FormTypeEnum.DATE); + } else if (fieldConfig.getColumnType().equals("datetime")) { + fieldConfig.setFormType(FormTypeEnum.DATE_TIME); + } else { + fieldConfig.setFormType(FormTypeEnum.INPUT); + } + + fieldConfig.setQueryType(QueryTypeEnum.EQ); + fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength()); + return fieldConfig; + } + + /** + * 保存代码生成配置 + * + * @param formData 代码生成配置表单 + */ + @Override + public void saveGenConfig(GenConfigForm formData) { + GenConfig genConfig = codegenConverter.toGenConfig(formData); + this.saveOrUpdate(genConfig); + + // 如果选择上级菜单且当前环境不是生产环境,则保存菜单 + Long parentMenuId = formData.getParentMenuId(); + if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { + menuService.addMenuForCodegen(parentMenuId, genConfig); + } + + List genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + + if (CollectionUtil.isEmpty(genFieldConfigs)) { + throw new BusinessException("字段配置不能为空"); + } + genFieldConfigs.forEach(genFieldConfig -> { + genFieldConfig.setConfigId(genConfig.getId()); + }); + genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + } + + /** + * 删除代码生成配置 + * + * @param tableName 表名 + */ + @Override + public void deleteGenConfig(String tableName) { + GenConfig genConfig = this.getOne(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName)); + + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenConfig::getTableName, tableName) + ); + if (result) { + genFieldConfigService.remove(new LambdaQueryWrapper() + .eq(GenFieldConfig::getConfigId, genConfig.getId()) + ); + } + } + + + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4e1148a7ee1594d49aa240b9de52e9a837c1c313 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/java/tech/hypersense/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -0,0 +1,20 @@ +package tech.hypersense.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.codegen.mapper.GenFieldConfigMapper; +import tech.hypersense.codegen.model.entity.GenFieldConfig; +import tech.hypersense.codegen.service.GenFieldConfigService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 代码生成字段配置服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { + +} diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000000000000000000000000000000000..e8cbe07082e79a379ccd47a7c97fcd514fb42e79 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +tech.hypersense.codegen.config.properties.CodegenProperties + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..4b5e6048f350d91fe63ba1f7bf3b33c9613a14a1 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/DatabaseMapper.xml @@ -0,0 +1,69 @@ + + + + + + + SELECT + t1.TABLE_NAME , + t1.TABLE_COMMENT , + t1.TABLE_COLLATION, + t1.ENGINE, + t1.CREATE_TIME, + CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured + FROM + information_schema.tables t1 + LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + WHERE + t1.TABLE_SCHEMA = (SELECT DATABASE()) + AND t1.table_type = 'BASE TABLE' + + AND t1.TABLE_NAME LIKE CONCAT('%',#{queryParams.keywords},'%') + + + + AND t1.TABLE_NAME NOT IN + + #{excludeTable} + + + ORDER BY + CREATE_TIME DESC + + + + SELECT + TABLE_NAME , + TABLE_COMMENT , + TABLE_COLLATION, + ENGINE, + CREATE_TIME + FROM + information_schema.tables + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + + + + SELECT + COLUMN_NAME, + DATA_TYPE, + COLUMN_COMMENT, + CASE COLUMN_KEY WHEN 'PRI' THEN 1 ELSE 0 END AS isPrimaryKey, + IS_NULLABLE, + CHARACTER_MAXIMUM_LENGTH, + CHARACTER_SET_NAME, + COLLATION_NAME + FROM + information_schema.columns + WHERE + TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_NAME = #{tableName} + ORDER BY ORDINAL_POSITION ASC + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..a43406b854e4d24911d9bef8e0ec18b31ba423a5 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..3e882b82d1bea893146f74a5be045957c400d764 --- /dev/null +++ b/hypersense-modules/hypersense-codegen/src/main/resources/mapper/GenFieldConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/hypersense-modules/hypersense-system/pom.xml b/hypersense-modules/hypersense-system/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d2433cb3d47b8fb370e401c23764203905407474 --- /dev/null +++ b/hypersense-modules/hypersense-system/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + tech.hypersense + hypersense-modules + ${revision} + + hypersense-system + + + tech.hypersense + hypersense-common-core + + + + tech.hypersense + hypersense-common-security + + + + tech.hypersense + hypersense-common-log + + + + tech.hypersense + hypersense-common-websocket + + + + tech.hypersense + hypersense-common-web + + + + tech.hypersense + hypersense-shared-redis + + + + tech.hypersense + hypersense-shared-sms + + + + tech.hypersense + hypersense-shared-mail + + + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java new file mode 100644 index 0000000000000000000000000000000000000000..125c6522f57f842899f6842474048564e65669b0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/ConfigController.java @@ -0,0 +1,87 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; +import tech.hypersense.system.model.query.ConfigPageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置前端控制层 + * @Version: 1.0 + */ +@Slf4j +@RestController +@RequiredArgsConstructor +@Tag(name = "10.系统配置") +@RequestMapping("/api/v1/config") +public class ConfigController { + + private final ConfigService configService; + + @Operation(summary = "系统配置分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:config:query')") + @Log( value = "系统配置分页列表",module = LogModuleEnum.SETTING) + public PageResult page(@ParameterObject ConfigPageQuery configPageQuery) { + IPage result = configService.page(configPageQuery); + return PageResult.success(result); + } + + @Operation(summary = "新增系统配置") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:config:add')") + @Log( value = "新增系统配置",module = LogModuleEnum.SETTING) + public Result> save(@RequestBody @Valid ConfigForm configForm) { + return Result.judge(configService.save(configForm)); + } + + @Operation(summary = "获取系统配置表单数据") + @GetMapping("/{id}/form") + public Result getConfigForm( + @Parameter(description = "系统配置ID") @PathVariable Long id + ) { + ConfigForm formData = configService.getConfigFormData(id); + return Result.success(formData); + } + + @Operation(summary = "刷新系统配置缓存") + @PutMapping("/refresh") + @PreAuthorize("@ss.hasPerm('sys:config:refresh')") + @Log( value = "刷新系统配置缓存",module = LogModuleEnum.SETTING) + public Result refreshCache() { + return Result.judge(configService.refreshCache()); + } + + @Operation(summary = "修改系统配置") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:update')") + @Log( value = "修改系统配置",module = LogModuleEnum.SETTING) + public Result> update(@Valid @PathVariable Long id, @RequestBody ConfigForm configForm) { + return Result.judge(configService.edit(id, configForm)); + } + + @Operation(summary = "删除系统配置") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:config:delete')") + @Log( value = "删除系统配置",module = LogModuleEnum.SETTING) + public Result> delete(@PathVariable Long id) { + return Result.judge(configService.delete(id)); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java new file mode 100644 index 0000000000000000000000000000000000000000..c5e1d43f0cd4f183a8d0cdad0fc2a250745709cf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DeptController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; +import tech.hypersense.system.service.DeptService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门控制器 + * @Version: 1.0 + */ +@Tag(name = "05.部门接口") +@RestController +@RequestMapping("/api/v1/dept") +@RequiredArgsConstructor +public class DeptController { + + private final DeptService deptService; + + @Operation(summary = "部门列表") + @GetMapping + @Log( value = "部门列表",module = LogModuleEnum.DEPT) + public Result> getDeptList( + DeptQuery queryParams + ) { + List list = deptService.getDeptList(queryParams); + return Result.success(list); + } + + @Operation(summary = "部门下拉列表") + @GetMapping("/options") + public Result>> getDeptOptions() { + List> list = deptService.listDeptOptions(); + return Result.success(list); + } + + @Operation(summary = "新增部门") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dept:add')") + @Debounce + public Result> saveDept( + @Valid @RequestBody DeptForm formData + ) { + Long id = deptService.saveDept(formData); + return Result.success(id); + } + + @Operation(summary = "获取部门表单数据") + @GetMapping("/{deptId}/form") + public Result getDeptForm( + @Parameter(description ="部门ID") @PathVariable Long deptId + ) { + DeptForm deptForm = deptService.getDeptForm(deptId); + return Result.success(deptForm); + } + + @Operation(summary = "修改部门") + @PutMapping(value = "/{deptId}") + @PreAuthorize("@ss.hasPerm('sys:dept:edit')") + public Result> updateDept( + @PathVariable Long deptId, + @Valid @RequestBody DeptForm formData + ) { + deptId = deptService.updateDept(deptId, formData); + return Result.success(deptId); + } + + @Operation(summary = "删除部门") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dept:delete')") + public Result> deleteDepartments( + @Parameter(description ="部门ID,多个以英文逗号(,)分割") @PathVariable("ids") String ids + ) { + boolean result = deptService.deleteByIds(ids); + return Result.judge(result); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java new file mode 100644 index 0000000000000000000000000000000000000000..51e3e6982eba2149df7edae9dc065b6ec88f55e0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictController.java @@ -0,0 +1,95 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; +import tech.hypersense.system.service.DictService; + +import java.util.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典控制层 + * @Version: 1.0 + */ +@Tag(name = "06.字典接口") +@RestController +@RequestMapping("/api/v1/dict") +@RequiredArgsConstructor +public class DictController { + + private final DictService dictService; + + @Operation(summary = "字典分页列表") + @GetMapping("/page") + @Log( value = "字典分页列表",module = LogModuleEnum.DICT) + public PageResult getDictPage( + DictPageQuery queryParams + ) { + Page result = dictService.getDictPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "所有字典列表") + @GetMapping("/list") + public Result> getAllDictWithData() { + List list = dictService.getAllDictWithData(); + return Result.success(list); + } + + @Operation(summary = "字典表单") + @GetMapping("/{id}/form") + public Result getDictForm( + @Parameter(description = "字典ID") @PathVariable Long id + ) { + DictForm formData = dictService.getDictForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict:add')") + @Debounce + public Result> saveDict(@Valid @RequestBody DictForm formData) { + boolean result = dictService.saveDict(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict:edit')") + public Result> updateDict( + @PathVariable Long id, + @RequestBody DictForm DictForm + ) { + boolean status = dictService.updateDict(id, DictForm); + return Result.judge(status); + } + + @Operation(summary = "删除字典") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict:delete')") + public Result> deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList()); + return Result.success(); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0466b527166688c43c36e920bcd6df0e1c195e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/DictDataController.java @@ -0,0 +1,97 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; +import tech.hypersense.system.service.DictDataService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据控制层 + * @Version: 1.0 + */ +@Tag(name = "07.字典数据接口") +@RestController +@RequestMapping("/api/v1/dict-data") +@RequiredArgsConstructor +public class DictDataController { + + private final DictDataService dictDataService; + + @Operation(summary = "字典数据分页列表") + @GetMapping("/page") + @Log( value = "字典数据分页列表",module = LogModuleEnum.DICT) + public PageResult getDictDataPage( + DictDataPageQuery queryParams + ) { + Page result = dictDataService.getDictDataPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取字典数据表单") + @GetMapping("/{id}/form") + public Result getDictDataForm( + @Parameter(description = "字典数据ID") @PathVariable Long id + ) { + DictDataForm formData = dictDataService.getDictDataForm(id); + return Result.success(formData); + } + + @Operation(summary = "新增字典数据") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:dict-data:add')") + @Debounce + public Result saveDictData(@Valid @RequestBody DictDataForm formData) { + boolean result = dictDataService.saveDictData(formData); + return Result.judge(result); + } + + @Operation(summary = "修改字典数据") + @PutMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:edit')") + public Result> updateDictData( + @PathVariable Long id, + @RequestBody DictDataForm formData + ) { + boolean status = dictDataService.updateDictData(formData); + return Result.judge(status); + } + + @Operation(summary = "删除字典数据") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:dict-data:delete')") + public Result deleteDictionaries( + @Parameter(description = "字典ID,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + dictDataService.deleteDictDataByIds(ids); + return Result.success(); + } + + @Operation(summary = "字典数据列表") + @GetMapping("/{dictCode}/options") + public Result>> getDictDataList( + @Parameter(description = "字典编码") @PathVariable String dictCode + ) { + List> options = dictDataService.getDictDataList(dictCode); + return Result.success(options); + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..39dafaa111d0e6154880221b3eda95db2c42bd4e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/LogController.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; +import tech.hypersense.system.model.vo.VisitStatsVO; +import tech.hypersense.system.model.vo.VisitTrendVO; +import tech.hypersense.system.service.LogService; + +import java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 日志控制层 + * @Version: 1.0 + */ +@Tag(name = "13.日志接口") +@RestController +@RequestMapping("/api/v1/logs") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @Operation(summary = "日志分页列表") + @GetMapping("/page") + public PageResult getLogPage( + LogPageQuery queryParams + ) { + Page result = logService.getLogPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "获取访问趋势") + @GetMapping("/visit-trend") + public Result getVisitTrend( + @Parameter(description = "开始时间", example = "yyyy-MM-dd") @RequestParam String startDate, + @Parameter(description = "结束时间", example = "yyyy-MM-dd") @RequestParam String endDate + ) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + VisitTrendVO data = logService.getVisitTrend(start, end); + return Result.success(data); + } + + @Operation(summary = "获取访问统计") + @GetMapping("/visit-stats") + public Result getVisitStats() { + VisitStatsVO result = logService.getVisitStats(); + return Result.success(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java new file mode 100644 index 0000000000000000000000000000000000000000..a9712bbbfb17fe184aed3ec07ff5337598788af8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/MenuController.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.query.MenuQuery; +import tech.hypersense.system.model.vo.MenuVO; +import tech.hypersense.system.model.vo.RouteVO; +import tech.hypersense.system.service.MenuService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单控制层 + * @Version: 1.0 + */ +@Tag(name = "04.菜单接口") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@Slf4j +public class MenuController { + + private final MenuService menuService; + + @Operation(summary = "菜单列表") + @GetMapping + @Log( value = "菜单列表",module = LogModuleEnum.MENU) + public Result> listMenus(MenuQuery queryParams) { + List menuList = menuService.listMenus(queryParams); + return Result.success(menuList); + } + + @Operation(summary = "菜单下拉列表") + @GetMapping("/options") + public Result>> listMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent + ) { + List> menus = menuService.listMenuOptions(onlyParent); + return Result.success(menus); + } + + @Operation(summary = "菜单路由列表") + @GetMapping("/routes") + public Result> getCurrentUserRoutes() { + List routeList = menuService.getCurrentUserRoutes(); + return Result.success(routeList); + } + + @Operation(summary = "菜单表单数据") + @GetMapping("/{id}/form") + public Result getMenuForm( + @Parameter(description = "菜单ID") @PathVariable Long id + ) { + MenuForm menu = menuService.getMenuForm(id); + return Result.success(menu); + } + + @Operation(summary = "新增菜单") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:menu:add')") + @Debounce + public Result> addMenu(@RequestBody MenuForm menuForm) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "修改菜单") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") + public Result> updateMenu( + @RequestBody MenuForm menuForm + ) { + boolean result = menuService.saveMenu(menuForm); + return Result.judge(result); + } + + @Operation(summary = "删除菜单") + @DeleteMapping("/{id}") + @PreAuthorize("@ss.hasPerm('sys:menu:delete')") + public Result> deleteMenu( + @Parameter(description = "菜单ID,多个以英文(,)分割") @PathVariable("id") Long id + ) { + boolean result = menuService.deleteMenu(id); + return Result.judge(result); + } + + @Operation(summary = "修改菜单显示状态") + @PatchMapping("/{menuId}") + public Result> updateMenuVisible( + @Parameter(description = "菜单ID") @PathVariable Long menuId, + @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible + + ) { + boolean result = menuService.updateMenuVisible(menuId, visible); + return Result.judge(result); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java new file mode 100644 index 0000000000000000000000000000000000000000..8e0c1c1f6e2852d065ab28c59edabd4257f07922 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/NoticeController.java @@ -0,0 +1,130 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; +import tech.hypersense.system.service.NoticeService; +import tech.hypersense.system.service.UserNoticeService; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告前端控制层 + * @Version: 1.0 + */ +@Tag(name = "12.通知公告接口") +@RestController +@RequestMapping("/api/v1/notices") +@RequiredArgsConstructor +public class NoticeController { + + private final NoticeService noticeService; + + private final UserNoticeService userNoticeService; + + @Operation(summary = "通知公告分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPerm('sys:notice:query')") + public PageResult getNoticePage(NoticePageQuery queryParams) { + IPage result = noticeService.getNoticePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增通知公告") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:notice:add')") + public Result> saveNotice(@RequestBody @Valid NoticeForm formData) { + boolean result = noticeService.saveNotice(formData); + return Result.judge(result); + } + + @Operation(summary = "获取通知公告表单数据") + @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result getNoticeForm( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeForm formData = noticeService.getNoticeFormData(id); + return Result.success(formData); + } + + @Operation(summary = "阅读获取通知公告详情") + @GetMapping("/{id}/detail") + public Result getNoticeDetail( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + NoticeDetailVO detailVO = noticeService.getNoticeDetail(id); + return Result.success(detailVO); + } + + @Operation(summary = "修改通知公告") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:notice:edit')") + public Result updateNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id, + @RequestBody @Validated NoticeForm formData + ) { + boolean result = noticeService.updateNotice(id, formData); + return Result.judge(result); + } + + @Operation(summary = "发布通知公告") + @PutMapping("/{id}/publish") + @PreAuthorize("@ss.hasPerm('sys:notice:publish')") + public Result publishNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.publishNotice(id); + return Result.judge(result); + } + + @Operation(summary = "撤回通知公告") + @PutMapping("/{id}/revoke") + @PreAuthorize("@ss.hasPerm('sys:notice:revoke')") + public Result revokeNotice( + @Parameter(description = "通知公告ID") @PathVariable Long id + ) { + boolean result = noticeService.revokeNotice(id); + return Result.judge(result); + } + + @Operation(summary = "删除通知公告") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:notice:delete')") + public Result deleteNotices( + @Parameter(description = "通知公告ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = noticeService.deleteNotices(ids); + return Result.judge(result); + } + + @Operation(summary = "全部已读") + @PutMapping("/read-all") + public Result readAll() { + userNoticeService.readAll(); + return Result.success(); + } + + @Operation(summary = "获取我的通知公告分页列表") + @GetMapping("/my-page") + public PageResult getMyNoticePage( + NoticePageQuery queryParams + ) { + IPage result = noticeService.getMyNoticePage(queryParams); + return PageResult.success(result); + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java new file mode 100644 index 0000000000000000000000000000000000000000..e073acba9703f685ec379412dfda1908449a5103 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/RoleController.java @@ -0,0 +1,119 @@ +package tech.hypersense.system.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; +import tech.hypersense.system.service.RoleService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色控制层 + * @Version: 1.0 + */ +@Tag(name = "03.角色接口") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +public class RoleController { + + private final RoleService roleService; + + @Operation(summary = "角色分页列表") + @GetMapping("/page") + @Log(value = "角色分页列表", module = LogModuleEnum.ROLE) + public PageResult getRolePage( + RolePageQuery queryParams + ) { + Page result = roleService.getRolePage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "角色下拉列表") + @GetMapping("/options") + public Result>> listRoleOptions() { + List> list = roleService.listRoleOptions(); + return Result.success(list); + } + + @Operation(summary = "新增角色") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:role:add')") + @Debounce + public Result> addRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "角色表单数据") + @GetMapping("/{roleId}/form") + public Result getRoleForm( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + RoleForm roleForm = roleService.getRoleForm(roleId); + return Result.success(roleForm); + } + + @Operation(summary = "修改角色") + @PutMapping(value = "/{id}") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") + public Result> updateRole(@Valid @RequestBody RoleForm roleForm) { + boolean result = roleService.saveRole(roleForm); + return Result.judge(result); + } + + @Operation(summary = "删除角色") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:role:delete')") + public Result deleteRoles( + @Parameter(description = "删除角色,多个以英文逗号(,)拼接") @PathVariable String ids + ) { + roleService.deleteRoles(ids); + return Result.success(); + } + + @Operation(summary = "修改角色状态") + @PutMapping(value = "/{roleId}/status") + public Result> updateRoleStatus( + @Parameter(description = "角色ID") @PathVariable Long roleId, + @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = roleService.updateRoleStatus(roleId, status); + return Result.judge(result); + } + + @Operation(summary = "获取角色的菜单ID集合") + @GetMapping("/{roleId}/menuIds") + public Result> getRoleMenuIds( + @Parameter(description = "角色ID") @PathVariable Long roleId + ) { + List menuIds = roleService.getRoleMenuIds(roleId); + return Result.success(menuIds); + } + + @Operation(summary = "分配菜单(包括按钮权限)给角色") + @PutMapping("/{roleId}/menus") + public Result assignMenusToRole( + @PathVariable Long roleId, + @RequestBody List menuIds + ) { + roleService.assignMenusToRole(roleId, menuIds); + return Result.success(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..301fd0145aa1d55167953c0c92d39031db6b2f6b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/controller/UserController.java @@ -0,0 +1,255 @@ +package tech.hypersense.system.controller; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.domain.result.PageResult; +import tech.hypersense.common.core.domain.result.Result; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.utils.ExcelUtils; +import tech.hypersense.common.log.annotation.Log; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.web.annotation.Debounce; +import tech.hypersense.system.listener.UserImportListener; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.*; +import tech.hypersense.system.model.query.UserPageQuery; +import tech.hypersense.system.model.vo.UserInfoVO; +import tech.hypersense.system.model.vo.UserPageVO; +import tech.hypersense.system.model.vo.UserProfileVO; +import tech.hypersense.system.service.UserService; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户控制层 + * @Version: 1.0 + */ +@Tag(name = "02.用户接口") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "用户分页列表") + @GetMapping("/page") + @Log(value = "用户分页列表", module = LogModuleEnum.USER) + public PageResult