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[] 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 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 @@ + + + + + + + + + + + + + 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 getUserPage( + @Valid UserPageQuery queryParams + ) { + IPage result = userService.getUserPage(queryParams); + return PageResult.success(result); + } + + @Operation(summary = "新增用户") + @PostMapping + @PreAuthorize("@ss.hasPerm('sys:user:add')") + @Debounce + @Log(value = "新增用户", module = LogModuleEnum.USER) + public Result saveUser( + @RequestBody @Valid UserForm userForm + ) { + boolean result = userService.saveUser(userForm); + return Result.judge(result); + } + + @Operation(summary = "用户表单数据") + @GetMapping("/{userId}/form") + @Log(value = "用户表单数据", module = LogModuleEnum.USER) + public Result getUserForm( + @Parameter(description = "用户ID") @PathVariable Long userId + ) { + UserForm formData = userService.getUserFormData(userId); + return Result.success(formData); + } + + @Operation(summary = "修改用户") + @PutMapping(value = "/{userId}") + @PreAuthorize("@ss.hasPerm('sys:user:edit')") + @Log(value = "修改用户", module = LogModuleEnum.USER) + public Result updateUser( + @Parameter(description = "用户ID") @PathVariable Long userId, + @RequestBody @Valid UserForm userForm + ) { + boolean result = userService.updateUser(userId, userForm); + return Result.judge(result); + } + + @Operation(summary = "删除用户") + @DeleteMapping("/{ids}") + @PreAuthorize("@ss.hasPerm('sys:user:delete')") + @Log(value = "删除用户", module = LogModuleEnum.USER) + public Result deleteUsers( + @Parameter(description = "用户ID,多个以英文逗号(,)分割") @PathVariable String ids + ) { + boolean result = userService.deleteUsers(ids); + return Result.judge(result); + } + + @Operation(summary = "修改用户状态") + @PatchMapping(value = "/{userId}/status") + @Log(value = "修改用户状态", module = LogModuleEnum.USER) + public Result updateUserStatus( + @Parameter(description = "用户ID") @PathVariable Long userId, + @Parameter(description = "用户状态(1:启用;0:禁用)") @RequestParam Integer status + ) { + boolean result = userService.update(new LambdaUpdateWrapper() + .eq(User::getId, userId) + .set(User::getStatus, status) + ); + return Result.judge(result); + } + + @Operation(summary = "获取当前登录用户信息") + @GetMapping("/me") + @Log(value = "获取当前登录用户信息", module = LogModuleEnum.USER) + public Result getCurrentUserInfo() { + UserInfoVO userInfoVO = userService.getCurrentUserInfo(); + return Result.success(userInfoVO); + } + + @Operation(summary = "用户导入模板下载") + @GetMapping("/template") + @Log(value = "用户导入模板下载", module = LogModuleEnum.USER) + public void downloadTemplate(HttpServletResponse response) throws IOException { + String fileName = "用户导入模板.xlsx"; + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); + + String fileClassPath = "templates" + File.separator + "excel" + File.separator + fileName; + InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileClassPath); + + ServletOutputStream outputStream = response.getOutputStream(); + ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(inputStream).build(); + + excelWriter.finish(); + } + + @Operation(summary = "导入用户") + @PostMapping("/import") + @Log(value = "导入用户", module = LogModuleEnum.USER) + public Result importUsers(MultipartFile file) throws IOException { + UserImportListener listener = new UserImportListener(); + ExcelUtils.importExcel(file.getInputStream(), UserImportDTO.class, listener); + return Result.success(listener.getExcelResult()); + } + + @Operation(summary = "导出用户") + @GetMapping("/export") + @Log(value = "导出用户", module = LogModuleEnum.USER) + public void exportUsers(UserPageQuery queryParams, HttpServletResponse response) throws IOException { + String fileName = "用户列表.xlsx"; + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8)); + + List exportUserList = userService.listExportUsers(queryParams); + EasyExcel.write(response.getOutputStream(), UserExportDTO.class).sheet("用户列表") + .doWrite(exportUserList); + } + + @Operation(summary = "获取个人中心用户信息") + @GetMapping("/profile") + @Log(value = "获取个人中心用户信息", module = LogModuleEnum.USER) + public Result getUserProfile() { + Long userId = SecurityUtils.getUserId(); + UserProfileVO userProfile = userService.getUserProfile(userId); + return Result.success(userProfile); + } + + @Operation(summary = "个人中心修改用户信息") + @PutMapping("/profile") + @Log(value = "个人中心修改用户信息", module = LogModuleEnum.USER) + public Result updateUserProfile(@RequestBody UserProfileForm formData) { + boolean result = userService.updateUserProfile(formData); + return Result.judge(result); + } + + @Operation(summary = "重置用户密码") + @PutMapping(value = "/{userId}/password/reset") + @PreAuthorize("@ss.hasPerm('sys:user:password:reset')") + public Result resetPassword( + @Parameter(description = "用户ID") @PathVariable Long userId, + @RequestParam String password + ) { + boolean result = userService.resetPassword(userId, password); + return Result.judge(result); + } + + @Operation(summary = "修改密码") + @PutMapping(value = "/password") + public Result changePassword( + @RequestBody PasswordUpdateForm data + ) { + Long currUserId = SecurityUtils.getUserId(); + boolean result = userService.changePassword(currUserId, data); + return Result.judge(result); + } + + @Operation(summary = "发送短信验证码(绑定或更换手机号)") + @PostMapping(value = "/mobile/code") + public Result sendMobileCode( + @Parameter(description = "手机号码", required = true) @RequestParam String mobile + ) { + boolean result = userService.sendMobileCode(mobile); + return Result.judge(result); + } + + @Operation(summary = "绑定或更换手机号") + @PutMapping(value = "/mobile") + public Result bindOrChangeMobile( + @RequestBody @Validated MobileUpdateForm data + ) { + boolean result = userService.bindOrChangeMobile(data); + return Result.judge(result); + } + + @Operation(summary = "发送邮箱验证码(绑定或更换邮箱)") + @PostMapping(value = "/email/code") + public Result sendEmailCode( + @Parameter(description = "邮箱地址", required = true) @RequestParam String email + ) { + userService.sendEmailCode(email); + return Result.success(); + } + + @Operation(summary = "绑定或更换邮箱") + @PutMapping(value = "/email") + public Result bindOrChangeEmail( + @RequestBody @Validated EmailUpdateForm data + ) { + boolean result = userService.bindOrChangeEmail(data); + return Result.judge(result); + } + + @Operation(summary = "用户下拉选项") + @GetMapping("/options") + public Result>> listUserOptions() { + List> list = userService.listUserOptions(); + return Result.success(list); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/ConfigConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/ConfigConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..2b470d609e17b5ec7dc16c67d3da90b1bdf5668d --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/ConfigConverter.java @@ -0,0 +1,23 @@ +package tech.hypersense.system.converter; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.mapstruct.Mapper; +import tech.hypersense.system.model.entity.Config; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.vo.ConfigVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置对象转换器 + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface ConfigConverter { + + Page toPageVo(Page page); + + Config toEntity(ConfigForm configForm); + + ConfigForm toForm(Config entity); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DeptConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DeptConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..a7cc28a6c31c3339b238249ccfb2b6fbaa1ad669 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DeptConverter.java @@ -0,0 +1,23 @@ +package tech.hypersense.system.converter; + +import org.mapstruct.Mapper; +import tech.hypersense.system.model.entity.Dept; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.vo.DeptVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门对象转换器 + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface DeptConverter { + + DeptForm toForm(Dept entity); + + DeptVO toVo(Dept entity); + + Dept toEntity(DeptForm deptForm); + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DictConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DictConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..45b5ea6c52ff2039bd51a2ed0c8173c85350627e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DictConverter.java @@ -0,0 +1,23 @@ +package tech.hypersense.system.converter; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.mapstruct.Mapper; +import tech.hypersense.system.model.entity.Dict; +import tech.hypersense.system.model.form.DictForm; +import tech.hypersense.system.model.vo.DictPageVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典 对象转换器 + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface DictConverter { + + Page toPageVo(Page page); + + DictForm toForm(Dict entity); + + Dict toEntity(DictForm entity); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DictDataConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DictDataConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..1b32ce32aa26e6b52702a43022cd4780a176295a --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/DictDataConverter.java @@ -0,0 +1,30 @@ +package tech.hypersense.system.converter; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.mapstruct.Mapper; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.system.model.entity.DictData; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.vo.DictPageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典项 对象转换器 + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface DictDataConverter { + + Page toPageVo(Page page); + + DictDataForm toForm(DictData entity); + + DictData toEntity(DictDataForm formFata); + + Option toOption(DictData dictData); + List> toOption(List dictData); +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/MenuConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/MenuConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..bf1641966168e6f2f0d910f8ef530e5b664f9f6e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/MenuConverter.java @@ -0,0 +1,26 @@ +package tech.hypersense.system.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import tech.hypersense.system.model.entity.Menu; +import tech.hypersense.system.model.form.MenuForm; +import tech.hypersense.system.model.vo.MenuVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单对象转换器 + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface MenuConverter { + + MenuVO toVo(Menu entity); + + @Mapping(target = "params", ignore = true) + MenuForm toForm(Menu entity); + + @Mapping(target = "params", ignore = true) + Menu toEntity(MenuForm menuForm); + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/NoticeConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/NoticeConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..c67520d429b439f84691633d977a0bbfe0b40005 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/NoticeConverter.java @@ -0,0 +1,40 @@ +package tech.hypersense.system.converter; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import tech.hypersense.system.model.bo.NoticeBO; +import tech.hypersense.system.model.entity.Notice; +import tech.hypersense.system.model.form.NoticeForm; +import tech.hypersense.system.model.vo.NoticeDetailVO; +import tech.hypersense.system.model.vo.NoticePageVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface NoticeConverter{ + + + @Mappings({ + @Mapping(target = "targetUserIds", expression = "java(cn.hutool.core.util.StrUtil.split(entity.getTargetUserIds(),\",\"))") + }) + default NoticeForm toForm(Notice entity) { + return null; + } + + @Mappings({ + @Mapping(target = "targetUserIds", expression = "java(cn.hutool.core.collection.CollUtil.join(formData.getTargetUserIds(),\",\"))") + }) + Notice toEntity(NoticeForm formData); + + NoticePageVO toPageVo(NoticeBO bo); + + Page toPageVo(Page noticePage); + + NoticeDetailVO toDetailVO(NoticeBO noticeBO); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/RoleConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/RoleConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..613de4c1c0c96f5d24f583ab15521622098f766e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/RoleConverter.java @@ -0,0 +1,36 @@ +package tech.hypersense.system.converter; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.system.model.entity.Role; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.vo.RolePageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色对象转换器 + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface RoleConverter { + + Page toPageVo(Page page); + + @Mappings({ + @Mapping(target = "value", source = "id"), + @Mapping(target = "label", source = "name") + }) + Option toOption(Role role); + + List> toOptions(List roles); + + Role toEntity(RoleForm roleForm); + + RoleForm toForm(Role entity); +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/UserConverter.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/UserConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..a8812775606d95c34f52536617c2651611a39a7a --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/converter/UserConverter.java @@ -0,0 +1,58 @@ +package tech.hypersense.system.converter; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.mapstruct.InheritInverseConfiguration; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.system.model.bo.UserBO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.UserForm; +import tech.hypersense.system.model.form.UserProfileForm; +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.model.dto.UserImportDTO; + + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 用户对象转换器 + * @Version: 1.0 + */ +@Mapper(componentModel = "spring") +public interface UserConverter { + + UserPageVO toPageVo(UserBO bo); + + Page toPageVo(Page bo); + + UserForm toForm(User entity); + + @InheritInverseConfiguration(name = "toForm") + User toEntity(UserForm entity); + + @Mappings({ + @Mapping(target = "userId", source = "id") + }) + UserInfoVO toUserInfoVo(User entity); + + User toEntity(UserImportDTO vo); + + + UserProfileVO toProfileVO(UserBO bo); + + User toEntity(UserProfileForm formData); + + @Mappings({ + @Mapping(target = "label", source = "nickname"), + @Mapping(target = "value", source = "id") + }) + Option toOption(User entity); + + List> toOptions(List list); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/DictCodeEnum.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/DictCodeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..e89d7a214d56007bff421f046a49eee9216ec1c8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/DictCodeEnum.java @@ -0,0 +1,28 @@ +package tech.hypersense.system.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 DictCodeEnum implements IBaseEnum { + + GENDER("gender", "性别"), + NOTICE_TYPE("notice_type", "通知类型"), + NOTICE_LEVEL("notice_level", "通知级别"); + + private final String value; + + private final String label; + + DictCodeEnum(String value, String label) { + this.value = value; + this.label = label; + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/MenuTypeEnum.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/MenuTypeEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..ce3271a31e7353d3d25da853172c3b06b9ba465a --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/MenuTypeEnum.java @@ -0,0 +1,34 @@ +package tech.hypersense.system.enums; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 菜单类型枚举 + * @Version: 1.0 + */ +@Getter +public enum MenuTypeEnum implements IBaseEnum { + + NULL(0, null), + MENU(1, "菜单"), + CATALOG(2, "目录"), + EXTLINK(3, "外链"), + BUTTON(4, "按钮"); + + // Mybatis-Plus 提供注解表示插入数据库时插入该值 + @EnumValue + private final Integer value; + + // @JsonValue // 表示对枚举序列化时返回此字段 + private final String label; + + MenuTypeEnum(Integer value, String label) { + this.value = value; + this.label = label; + } + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/NoticePublishStatusEnum.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/NoticePublishStatusEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..b25f2adf8cb63b84e127311d0b8818254a6d4715 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/NoticePublishStatusEnum.java @@ -0,0 +1,30 @@ +package tech.hypersense.system.enums; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通告发布状态枚举 + * @Version: 1.0 + */ +@Getter +@Schema(enumAsRef = true) +public enum NoticePublishStatusEnum implements IBaseEnum { + + UNPUBLISHED(0, "未发布"), + PUBLISHED(1, "已发布"), + REVOKED(-1, "已撤回"); + + + private final Integer value; + + private final String label; + + NoticePublishStatusEnum(Integer value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/NoticeTargetEnum.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/NoticeTargetEnum.java new file mode 100644 index 0000000000000000000000000000000000000000..a190d9196e3f0db5586a9dd1713cc4f987e49001 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/enums/NoticeTargetEnum.java @@ -0,0 +1,29 @@ +package tech.hypersense.system.enums; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import tech.hypersense.common.core.enums.base.IBaseEnum; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知目标类型枚举 + * @Version: 1.0 + */ +@Getter +@Schema(enumAsRef = true) +public enum NoticeTargetEnum implements IBaseEnum { + + ALL(1, "全体"), + SPECIFIED(2, "指定"); + + + private final Integer value; + + private final String label; + + NoticeTargetEnum(Integer value, String label) { + this.value = value; + this.label = label; + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/listener/UserImportListener.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/listener/UserImportListener.java new file mode 100644 index 0000000000000000000000000000000000000000..805f9c260d87d25f616fcd9ee2db7f05fa206f39 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/listener/UserImportListener.java @@ -0,0 +1,222 @@ +package tech.hypersense.system.listener; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.core.domain.result.ExcelResult; +import tech.hypersense.common.core.enums.StatusEnum; +import tech.hypersense.system.converter.UserConverter; +import tech.hypersense.system.enums.DictCodeEnum; +import tech.hypersense.system.model.dto.UserImportDTO; +import tech.hypersense.system.model.entity.*; +import tech.hypersense.system.service.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户导入监听器 + *

+ * 最简单的读的监听器 + * + * @Version: 1.0 + */ +@Slf4j +public class UserImportListener extends AnalysisEventListener { + + /** + * Excel 导入结果 + */ + @Getter + private final ExcelResult excelResult; + + private final UserService userService; + private final PasswordEncoder passwordEncoder; + private final UserConverter userConverter; + private final UserRoleService userRoleService; + + private final List roleList; + private final List deptList; + private final List genderList; + + /** + * 当前行 + */ + private Integer currentRow = 1; + + /** + * 构造方法 + *

在构造方法中给需要查询的内容查询好,尽量避免每条数据查询一次

+ */ + public UserImportListener() { + this.userService = SpringUtil.getBean(UserService.class); + this.passwordEncoder = SpringUtil.getBean(PasswordEncoder.class); + this.userRoleService = SpringUtil.getBean(UserRoleService.class); + this.userConverter = SpringUtil.getBean(UserConverter.class); + this.roleList = SpringUtil.getBean(RoleService.class) + .list(new LambdaQueryWrapper().eq(Role::getStatus, StatusEnum.ENABLE.getValue()) + .select(Role::getId, Role::getCode)); + this.deptList = SpringUtil.getBean(DeptService.class) + .list(new LambdaQueryWrapper().select(Dept::getId, Dept::getCode)); + this.genderList = SpringUtil.getBean(DictDataService.class) + .list(new LambdaQueryWrapper().eq(DictData::getDictCode, DictCodeEnum.GENDER.getValue())); + this.excelResult = new ExcelResult(); + } + + /** + * 每一条数据解析都会来调用 + *

+ * 1. 数据校验;全字段校验 + * 2. 数据持久化; + * + * @param userImportDTO 一行数据,类似于 {@link AnalysisContext#readRowHolder()} + */ + @Override + public void invoke(UserImportDTO userImportDTO, AnalysisContext analysisContext) { + log.info("解析到一条用户数据:{}", JSONUtil.toJsonStr(userImportDTO)); + + boolean validation = true; + String errorMsg = "第" + currentRow + "行数据校验失败:"; + String username = userImportDTO.getUsername(); + if (StrUtil.isBlank(username)) { + errorMsg += "用户名为空;"; + validation = false; + } else { + long count = userService.count(new LambdaQueryWrapper().eq(User::getUsername, username)); + if (count > 0) { + errorMsg += "用户名已存在;"; + validation = false; + } + } + + String nickname = userImportDTO.getNickname(); + if (StrUtil.isBlank(nickname)) { + errorMsg += "用户昵称为空;"; + validation = false; + } + + String mobile = userImportDTO.getMobile(); + if (StrUtil.isBlank(mobile)) { + errorMsg += "手机号码为空;"; + validation = false; + } else { + if (!Validator.isMobile(mobile)) { + errorMsg += "手机号码不正确;"; + validation = false; + } + } + + if (validation) { + // 校验通过,持久化至数据库 + User entity = userConverter.toEntity(userImportDTO); + entity.setPassword(passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD)); // 默认密码 + // 性别逆向翻译 根据字典标签得到字典值 + String genderLabel = userImportDTO.getGenderLabel(); + entity.setGender(getGenderValue(genderLabel)); + // 角色解析 + String roleCodes = userImportDTO.getRoleCodes(); + List roleIds = getRoleIds(roleCodes); + // 部门解析 + String deptCode = userImportDTO.getDeptCode(); + entity.setDeptId(getDeptId(deptCode)); + + boolean saveResult = userService.save(entity); + if (saveResult) { + excelResult.setValidCount(excelResult.getValidCount() + 1); + // 保存用户角色关联 + if (CollectionUtil.isNotEmpty(roleIds)) { + List userRoles = roleIds.stream() + .map(roleId -> new UserRole(entity.getId(), roleId)) + .collect(Collectors.toList()); + userRoleService.saveBatch(userRoles); + } + } else { + excelResult.setInvalidCount(excelResult.getInvalidCount() + 1); + errorMsg += "第" + currentRow + "行数据保存失败;"; + excelResult.getMessageList().add(errorMsg); + } + } else { + excelResult.setInvalidCount(excelResult.getInvalidCount() + 1); + excelResult.getMessageList().add(errorMsg); + } + currentRow++; + } + + + /** + * 根据角色编码获取角色ID + * + * @param roleCodes 角色编码 逗号分隔 + * @return 角色ID集合 + */ + private List getRoleIds(String roleCodes) { + if (StrUtil.isNotBlank(roleCodes)) { + String[] split = roleCodes.split(","); + if (split.length > 0) { + List roleIds = new ArrayList<>(); + for (String roleCode : split) { + this.roleList.stream().filter(r -> r.getCode().equals(roleCode)) + .findFirst().ifPresent(role -> roleIds.add(role.getId())); + } + return roleIds.stream().distinct().toList(); + } + } + return Collections.emptyList(); + } + + /** + * 根据部门编码获取部门ID + * + * @param deptCode 部门编码 + * @return 部门ID + */ + private Long getDeptId(String deptCode) { + if (StrUtil.isNotBlank(deptCode)) { + return this.deptList.stream().filter(r -> r.getCode().equals(deptCode)) + .findFirst().map(Dept::getId).orElse(null); + } + return null; + } + + /** + * 根据性别标签获取性别值 + * + * @param genderLabel 性别标签 + * @return 性别值 + */ + private Integer getGenderValue(String genderLabel) { + if (StrUtil.isNotBlank(genderLabel)) { + return this.genderList.stream() + .filter(r -> r.getLabel().equals(genderLabel)) + .findFirst() + .map(DictData::getValue) + .map(Convert::toInt) + .orElse(null); + } + return null; + } + + /** + * 所有数据解析完成会来调用 + */ + @Override + public void doAfterAllAnalysed(AnalysisContext analysisContext) { + log.info("所有数据解析完成!"); + } + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/ConfigMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/ConfigMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..7df1ad01b4823f8de4075c82e7b30ae431ea4702 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/ConfigMapper.java @@ -0,0 +1,16 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.system.model.entity.Config; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@Mapper +public interface ConfigMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DeptMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DeptMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..8f70da67ef7bac8f2288cfe9e5c9bff4cca72f20 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DeptMapper.java @@ -0,0 +1,25 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import tech.hypersense.common.core.annotation.DataPermission; +import tech.hypersense.system.model.entity.Dept; + +import java.util.List; + +/** +*@Author: HyperSense +*@CreateTime: 2025-03-25 +*@Description: +*@Version: 1.0 +*/ +@Mapper +public interface DeptMapper extends BaseMapper { + + @DataPermission(deptIdColumnName = "id") + @Override + List selectList(@Param(Constants.WRAPPER) Wrapper queryWrapper); +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DictDataMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DictDataMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..b950616a7db1d07cb421aa858e53a84637b8da85 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DictDataMapper.java @@ -0,0 +1,24 @@ +package tech.hypersense.system.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.system.model.entity.DictData; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典数据映射层 + * @Version: 1.0 + */ +@Mapper +public interface DictDataMapper extends BaseMapper { + + /** + * 字典数据分页列表 + */ + Page getDictDataPage(Page page, DictDataPageQuery queryParams); +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DictMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DictMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..c726a6d4074c78e1554979106bd24110c28df673 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/DictMapper.java @@ -0,0 +1,36 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import tech.hypersense.system.model.entity.Dict; +import tech.hypersense.system.model.query.DictPageQuery; +import tech.hypersense.system.model.vo.DictPageVO; +import tech.hypersense.system.model.vo.DictVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典 访问层 + * @Version: 1.0 + */ +public interface DictMapper extends BaseMapper { + + /** + * 字典分页列表 + * + * @param page 分页参数 + * @param queryParams 查询参数 + * @return 字典分页列表 + */ + Page getDictPage(Page page, DictPageQuery queryParams); + + /** + * 获取字典列表(包含字典数据) + * + * @return 字典列表 + */ + List getAllDictWithData(); + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/LogMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/LogMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..e9df3f2aa76124cc92adcfd3f66286fe8fab19e6 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/LogMapper.java @@ -0,0 +1,54 @@ +package tech.hypersense.system.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.system.model.bo.VisitCount; +import tech.hypersense.system.model.bo.VisitStatsBO; +import tech.hypersense.system.model.entity.Log; +import tech.hypersense.system.model.query.LogPageQuery; +import tech.hypersense.system.model.vo.LogPageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@Mapper +public interface LogMapper extends BaseMapper { + + /** + * 获取日志分页列表 + */ + Page getLogPage(Page page, LogPageQuery queryParams); + + /** + * 统计浏览数(PV) + * + * @param startDate 开始日期 yyyy-MM-dd + * @param endDate 结束日期 yyyy-MM-dd + */ + List getPvCounts(String startDate, String endDate); + + /** + * 统计IP数 + * + * @param startDate 开始日期 yyyy-MM-dd + * @param endDate 结束日期 yyyy-MM-dd + */ + List getIpCounts(String startDate, String endDate); + + /** + * 获取浏览量(PV)统计 + */ + VisitStatsBO getPvStats(); + + /** + * 获取访问IP统计 + */ + VisitStatsBO getUvStats(); +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/MenuMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/MenuMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..2a79f21a1a215441aecda392b6c8dc8bc00097d9 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/MenuMapper.java @@ -0,0 +1,26 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.system.model.entity.Menu; + +import java.util.List; +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 菜单访问层 + * @Version: 1.0 + */ +@Mapper +public interface MenuMapper extends BaseMapper

{ + + /** + * 获取菜单路由列表 + * + * @param roleCodes 角色编码集合 + */ + List getMenusByRoleCodes(Set roleCodes); + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/NoticeMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/NoticeMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..35da7f5a1a0a2a736b57eb780f5878654893d418 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/NoticeMapper.java @@ -0,0 +1,37 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import tech.hypersense.system.model.bo.NoticeBO; +import tech.hypersense.system.model.entity.Notice; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticePageVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知公告Mapper接口 + * @Version: 1.0 + */ +@Mapper +public interface NoticeMapper extends BaseMapper { + + /** + * 获取通知公告分页数据 + * + * @param page 分页对象 + * @param queryParams 查询参数 + * @return 通知公告分页数据 + */ + Page getNoticePage(Page page, NoticePageQuery queryParams); + + /** + * 获取阅读时通知公告详情 + * + * @param id 通知公告ID + * @return 通知公告详情 + */ + NoticeBO getNoticeDetail(@Param("id") Long id); +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/RoleMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/RoleMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..3cb14784e549ccc3d3819d5de5f05e92e06b0494 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/RoleMapper.java @@ -0,0 +1,26 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.system.model.entity.Role; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@Mapper +public interface RoleMapper extends BaseMapper { + + + /** + * 获取最大范围的数据权限 + * + * @param roles + * @return + */ + Integer getMaximumDataScope(Set roles); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/RoleMenuMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/RoleMenuMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..ac2b6e012f08fed30c6f185a036ee9497ce84384 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/RoleMenuMapper.java @@ -0,0 +1,41 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.system.model.bo.RolePermsBO; +import tech.hypersense.system.model.entity.RoleMenu; + +import java.util.List; +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 角色菜单访问层 + * @Version: 1.0 + */ +@Mapper +public interface RoleMenuMapper extends BaseMapper { + + /** + * 获取角色拥有的菜单ID集合 + * + * @param roleId 角色ID + * @return 菜单ID集合 + */ + List listMenuIdsByRoleId(Long roleId); + + /** + * 获取权限和拥有权限的角色列表 + */ + List getRolePermsList(String roleCode); + + + /** + * 获取角色权限集合 + * + * @param roles + * @return + */ + Set listRolePerms(Set roles); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..581d0d23544f20cbbda1b396a785a75e7a1eb551 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserMapper.java @@ -0,0 +1,84 @@ +package tech.hypersense.system.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.common.core.annotation.DataPermission; +import tech.hypersense.system.model.bo.UserBO; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; +import tech.hypersense.system.model.dto.UserExportDTO; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.form.UserForm; +import tech.hypersense.system.model.query.UserPageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户持久层接口 + * @Version: 1.0 + */ +@Mapper +public interface UserMapper extends BaseMapper { + + /** + * 获取用户分页列表 + * + * @param page 分页参数 + * @param queryParams 查询参数 + * @return 用户分页列表 + */ + @DataPermission(deptAlias = "u", userAlias = "u") + Page getUserPage(Page page, UserPageQuery queryParams); + + /** + * 获取用户表单详情 + * + * @param userId 用户ID + * @return 用户表单详情 + */ + UserForm getUserFormData(Long userId); + + /** + * 根据用户名获取认证信息 + * + * @param username 用户名 + * @return 认证信息 + */ + UserAuthInfo getUserAuthInfo(String username); + + /** + * 根据微信openid获取用户认证信息 + * + * @param openid 微信openid + * @return 认证信息 + */ + UserAuthInfo getUserAuthInfoByOpenId(String openid); + + /** + * 根据手机号获取用户认证信息 + * + * @param mobile 手机号 + * @return 认证信息 + */ + UserAuthInfo getUserAuthInfoByMobile(String mobile); + + /** + * 获取导出用户列表 + * + * @param queryParams 查询参数 + * @return 导出用户列表 + */ + @DataPermission(deptAlias = "u", userAlias = "u") + List listExportUsers(UserPageQuery queryParams); + + /** + * 获取用户个人中心信息 + * + * @param userId 用户ID + * @return 用户个人中心信息 + */ + UserBO getUserProfile(Long userId); + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserNoticeMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserNoticeMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..3d33321a916827d2b43e1c7d768c25365a1c2bd0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserNoticeMapper.java @@ -0,0 +1,28 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import tech.hypersense.system.model.entity.UserNotice; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户公告状态Mapper接口 + * @Version: 1.0 + */ +@Mapper +public interface UserNoticeMapper extends BaseMapper { + /** + * 分页获取我的通知公告 + * @param page 分页对象 + * @param queryParams 查询参数 + * @return 通知公告分页列表 + */ + IPage getMyNoticePage(Page page, @Param("queryParams") NoticePageQuery queryParams); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserRoleMapper.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserRoleMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..1a8e11150cdb872d3b4cb006c5c81062f2138cb5 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/mapper/UserRoleMapper.java @@ -0,0 +1,22 @@ +package tech.hypersense.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import tech.hypersense.system.model.entity.UserRole; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户角色访问层 + * @Version: 1.0 + */ +@Mapper +public interface UserRoleMapper extends BaseMapper { + + /** + * 获取角色绑定的用户数 + * + * @param roleId 角色ID + */ + int countUsersForRole(Long roleId); +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/LogBo.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/LogBo.java new file mode 100644 index 0000000000000000000000000000000000000000..bc82e4f93f5f50ab4c6fa88dd5542188672142b8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/LogBo.java @@ -0,0 +1,104 @@ +package tech.hypersense.system.model.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import io.github.linpeilie.annotations.AutoMappers; +import lombok.Data; +import tech.hypersense.common.core.enums.LogModuleEnum; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.system.model.entity.Log; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 操作日志记录业务对象 + * @Version: 1.0 + */ +@Data +@AutoMappers({ + @AutoMapper(target = Log.class, reverseConvertGenerate = false), + @AutoMapper(target = OperateLogEvent.class) +}) +public class LogBo { + + /** + * 主键 + */ + 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-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/NoticeBO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/NoticeBO.java new file mode 100644 index 0000000000000000000000000000000000000000..71245e721ab579bb7129e3185976003e5dbcf752 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/NoticeBO.java @@ -0,0 +1,76 @@ +package tech.hypersense.system.model.bo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知公告业务对象 + * @Version: 1.0 + */ +@Data +public class NoticeBO { + + /** + * 通知ID + */ + private Long id; + + /** + * 通知标题 + */ + private String title; + + /** + * 通知类型 + */ + private Integer type; + + /** + * 通知类型标签 + */ + private String typeLabel; + + /** + * 通知内容 + */ + private String content; + + /** + * 发布人姓名 + */ + private String publisherName; + + /** + * 通知等级(L: 低, M: 中, H: 高) + */ + private String level; + + /** + * 目标类型(1: 全体 2: 指定) + */ + private Integer targetType; + + /** + * 发布状态(0: 未发布, 1: 已发布, -1: 已撤回) + */ + private Integer publishStatus; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 发布时间 + */ + private LocalDateTime publishTime; + + /** + * 撤回时间 + */ + private LocalDateTime revokeTime; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/RolePermsBO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/RolePermsBO.java new file mode 100644 index 0000000000000000000000000000000000000000..ee58feeb75dd203b33c8a750e2d025f742b151b5 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/RolePermsBO.java @@ -0,0 +1,26 @@ +package tech.hypersense.system.model.bo; + +import lombok.Data; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 角色权限业务对象 + * @Version: 1.0 + */ +@Data +public class RolePermsBO { + + /** + * 角色编码 + */ + private String roleCode; + + /** + * 权限标识集合 + */ + private Set perms; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/UserBO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/UserBO.java new file mode 100644 index 0000000000000000000000000000000000000000..d5cab88364687f99ff34bf17603f85f5e1cdd327 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/UserBO.java @@ -0,0 +1,71 @@ +package tech.hypersense.system.model.bo; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户持久化对象 + * @Version: 1.0 + */ +@Data +public class UserBO { + + /** + * 用户ID + */ + private Long id; + + /** + * 账户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 手机号 + */ + private String mobile; + + /** + * 性别(1->男;2->女) + */ + private Integer gender; + + /** + * 头像URL + */ + private String avatar; + + /** + * 邮箱 + */ + private String email; + + /** + * 状态: 1->启用;0->禁用 + */ + private Integer status; + + /** + * 部门名称 + */ + private String deptName; + + /** + * 角色名称,多个使用英文逗号(,)分割 + */ + private String roleNames; + + /** + * 创建时间 + */ + private LocalDateTime createTime; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/VisitCount.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/VisitCount.java new file mode 100644 index 0000000000000000000000000000000000000000..1ae92778629c08a3b49e22b17fed6d798cfc6c10 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/VisitCount.java @@ -0,0 +1,23 @@ +package tech.hypersense.system.model.bo; + +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 特定日期访问统计 + * @Version: 1.0 + */ +@Data +public class VisitCount { + + /** + * 日期 yyyy-MM-dd + */ + private String date; + + /** + * 访问次数 + */ + private Integer count; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/VisitStatsBO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/VisitStatsBO.java new file mode 100644 index 0000000000000000000000000000000000000000..b6d77528e110f89ecd7716421007ca997a6194b4 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/bo/VisitStatsBO.java @@ -0,0 +1,28 @@ +package tech.hypersense.system.model.bo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 访问量统计业务对象 + * @Version: 1.0 + */ +@Getter +@Setter +public class VisitStatsBO { + + @Schema(description = "今日访问量 (PV)") + private Integer todayCount; + + @Schema(description = "累计访问量 ") + private Integer totalCount; + + @Schema(description = "页面访问量增长率") + private BigDecimal growthRate; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/NoticeDTO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/NoticeDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..86261a6f657cbfa77f3dbb39e927e597941e82c6 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/NoticeDTO.java @@ -0,0 +1,31 @@ +package tech.hypersense.system.model.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知传送对象 + * @Version: 1.0 + */ +@Data +public class NoticeDTO { + + @Schema(description = "通知ID") + private Long id; + + @Schema(description = "通知类型") + private Integer type; + + @Schema(description = "通知标题") + private String title; + + @Schema(description = "通知时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime publishTime; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/UserExportDTO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/UserExportDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..dd68b38e125921d5e8c36f46765d78dda93a431c --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/UserExportDTO.java @@ -0,0 +1,43 @@ +package tech.hypersense.system.model.dto; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.annotation.format.DateTimeFormat; +import com.alibaba.excel.annotation.write.style.ColumnWidth; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户导出视图对象 + * @Version: 1.0 + */ +@Data +@ColumnWidth(20) +public class UserExportDTO { + + @ExcelProperty(value = "用户名") + private String username; + + @ExcelProperty(value = "用户昵称") + private String nickname; + + @ExcelProperty(value = "部门") + private String deptName; + + @ExcelProperty(value = "性别") + private String gender; + + @ExcelProperty(value = "手机号码") + private String mobile; + + @ExcelProperty(value = "邮箱") + private String email; + + @ExcelProperty(value = "创建时间") + @DateTimeFormat("yyyy/MM/dd HH:mm:ss") + private LocalDateTime createTime; + + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/UserImportDTO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/UserImportDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..0607d5afe68117db03c921949c322b1996fff25e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/dto/UserImportDTO.java @@ -0,0 +1,37 @@ +package tech.hypersense.system.model.dto; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户导入对象 + * @Version: 1.0 + */ +@Data +public class UserImportDTO { + + @ExcelProperty(value = "用户名") + private String username; + + @ExcelProperty(value = "昵称") + private String nickname; + + @ExcelProperty(value = "性别") + private String genderLabel; + + @ExcelProperty(value = "手机号码") + private String mobile; + + @ExcelProperty(value = "邮箱") + private String email; + + @ExcelProperty("角色") + private String roleCodes; + + @ExcelProperty("部门") + private String deptCode; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Config.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Config.java new file mode 100644 index 0000000000000000000000000000000000000000..95f4858d87e97f8ebd3144eac042748535e0477c --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Config.java @@ -0,0 +1,57 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import tech.hypersense.common.core.domain.model.base.BaseEntity; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 系统配置 实体 + * @Version: 1.0 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description = "系统配置") +@TableName("sys_config") +public class Config extends BaseEntity { + + /** + * 配置名称 + */ + private String configName; + + /** + * 配置键 + */ + private String configKey; + + /** + * 配置值 + */ + private String configValue; + + /** + * 描述、备注 + */ + private String remark; + + /** + * 创建人ID + */ + private Long createBy; + + /** + * 更新人ID + */ + private Long updateBy; + + /** + * 逻辑删除标识(0-未删除 1-已删除) + */ + private Integer isDeleted; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Dept.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Dept.java new file mode 100644 index 0000000000000000000000000000000000000000..b3da9e78fa52d6f0484861fe3d70efeb6ebaf0c9 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Dept.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.model.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-25 + * @Description: 部门实体 + * @Version: 1.0 + */ +@TableName("sys_dept") +@Getter +@Setter +public class Dept extends BaseEntity { + + /** + * 部门名称 + */ + private String name; + + /** + * 部门编码 + */ + private String code; + + /** + * 父节点id + */ + private Long parentId; + + /** + * 父节点id路径 + */ + private String treePath; + + /** + * 显示顺序 + */ + private Integer sort; + + /** + * 状态(1-正常 0-禁用) + */ + private Integer status; + + /** + * 创建人 ID + */ + private Long createBy; + + /** + * 更新人 ID + */ + private Long updateBy; + + /** + * 是否删除(0-否 1-是) + */ + private Integer isDeleted; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Dict.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Dict.java new file mode 100644 index 0000000000000000000000000000000000000000..7470d41ef322b7e8776857424b48a8b9c80f7e22 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Dict.java @@ -0,0 +1,45 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import tech.hypersense.common.core.domain.model.base.BaseEntity; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典实体 + * @Version: 1.0 + */ +@EqualsAndHashCode(callSuper = false) +@TableName("sys_dict") +@Data +public class Dict extends BaseEntity { + + /** + * 字典编码 + */ + private String dictCode; + + /** + * 字典名称 + */ + private String name; + + + /** + * 状态(1:启用, 0:停用) + */ + private Integer status; + + /** + * 备注 + */ + private String remark; + + /** + * 逻辑删除标识(0-未删除 1-已删除) + */ + private Integer isDeleted; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/DictData.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/DictData.java new file mode 100644 index 0000000000000000000000000000000000000000..e01636484887fc6bd45387e1a6af7c6d19302610 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/DictData.java @@ -0,0 +1,53 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import tech.hypersense.common.core.domain.model.base.BaseEntity; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典数据实体对象 + * @Version: 1.0 + */ +@EqualsAndHashCode(callSuper = false) +@TableName("sys_dict_data") +@Data +public class DictData extends BaseEntity { + + /** + * 字典编码 + */ + private String dictCode; + + /** + * 字典项名称 + */ + private String label; + + /** + * 字典项值 + */ + private String value; + + /** + * 排序 + */ + private Integer sort; + + /** + * 状态(1-正常,0-禁用) + */ + private Integer status; + + /** + * 备注 + */ + private String remark; + + /** + * 标签类型 + */ + private String tagType; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Log.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..b78cd18b48c9623b9a11c1b6bd24b0902b4c3cf0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Log.java @@ -0,0 +1,105 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import tech.hypersense.common.core.enums.LogModuleEnum; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 系统日志 实体类 + * @Version: 1.0 + */ +@Data +@TableName("sys_log") +public class Log implements Serializable { + + /** + * 主键 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 日志模块 + */ + private LogModuleEnum module; + + /** + * 请求方式 + */ + @TableField(value = "request_method") + private String requestMethod; + + /** + * 请求参数 + */ + @TableField(value = "request_params") + private String requestParams; + + /** + * 响应参数 + */ + @TableField(value = "response_content") + 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; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Menu.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Menu.java new file mode 100644 index 0000000000000000000000000000000000000000..0e2e0c7741fa5fa5975d7f0e3ad917e2aa26eb03 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Menu.java @@ -0,0 +1,112 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.system.enums.MenuTypeEnum; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 菜单实体 + * @Version: 1.0 + */ +@TableName("sys_menu") +@Getter +@Setter +public class Menu { + /** + * 菜单ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 父菜单ID + */ + private Long parentId; + + /** + * 菜单名称 + */ + private String name; + + /** + * 菜单类型(1-菜单;2-目录;3-外链;4-按钮权限) + */ + private MenuTypeEnum type; + + /** + * 路由名称(Vue Router 中定义的路由名称) + */ + private String routeName; + + /** + * 路由路径(Vue Router 中定义的 URL 路径) + */ + private String routePath; + + /** + * 组件路径(vue页面完整路径,省略.vue后缀) + */ + private String component; + + /** + * 权限标识 + */ + private String perm; + + /** + * 显示状态(1:显示;0:隐藏) + */ + private Integer visible; + + /** + * 排序 + */ + private Integer sort; + + /** + * 菜单图标 + */ + private String icon; + + /** + * 跳转路径 + */ + private String redirect; + + /** + * 父节点路径,以英文逗号(,)分割 + */ + private String treePath; + + /** + * 【菜单】是否开启页面缓存(1:开启;0:关闭) + */ + private Integer keepAlive; + + /** + * 【目录】只有一个子路由是否始终显示(1:是 0:否) + */ + private Integer alwaysShow; + + /** + * 路由参数 + */ + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private String params; + + + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Notice.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Notice.java new file mode 100644 index 0000000000000000000000000000000000000000..636b15017e59b0bd79dca1ed299e2df6c1481aa0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Notice.java @@ -0,0 +1,88 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; + +import java.io.Serial; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知公告实体对象 + * @Version: 1.0 + */ +@Getter +@Setter +@TableName("sys_notice") +public class Notice extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 通知标题 + */ + private String title; + /** + * 通知内容 + */ + private String content; + /** + * 通知类型 + */ + private Integer type; + + /** + * 发布人 + */ + private Long publisherId; + + /** + * 通知等级(L: 低, M: 中, H: 高) + */ + private String level; + + /** + * 目标类型(1: 全体, 2: 指定) + */ + private Integer targetType; + + /** + * 目标用户ID集合 + */ + private String targetUserIds; + + /** + * 发布状态(0: 未发布, 1: 已发布, -1: 已撤回) + */ + private Integer publishStatus; + + /** + * 发布时间 + */ + private LocalDateTime publishTime; + + /** + * 撤回时间 + */ + private LocalDateTime revokeTime; + + /** + * 创建人ID + */ + private Long createBy; + + /** + * 更新人ID + */ + private Long updateBy; + + /** + * 逻辑删除标识(0-未删除 1-已删除) + */ + private Integer isDeleted; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Project.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Project.java new file mode 100644 index 0000000000000000000000000000000000000000..fac0452497df2d63c4dd1b5d2b92fb231c7403e8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Project.java @@ -0,0 +1,60 @@ +package tech.hypersense.system.model.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-25 + * @Description: 项目实践实体对象 + * @Version: 1.0 + */ +@Getter +@Setter +@TableName("blog_project") +public class Project extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 项目名称 + */ + private String name; + /** + * 技术栈 + */ + private String techs; + /** + * 项目简介 + */ + private String description; + /** + * 项目地址 + */ + private String repo; + /** + * 项目内容 + */ + private String content; + /** + * 项目时间 + */ + private String date; + /** + * 创建人 ID + */ + private Long createBy; + + /** + * 更新人 ID + */ + private Long updateBy; + + /** + * 是否删除(0-否 1-是) + */ + private Integer isDeleted; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Role.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..9fc3c9b8661da9b783fd0a2ef80c4d233f81011b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/Role.java @@ -0,0 +1,58 @@ +package tech.hypersense.system.model.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-25 + * @Description: 角色实体 + * @Version: 1.0 + */ +@TableName("sys_role") +@Getter +@Setter +public class Role extends BaseEntity { + + /** + * 角色名称 + */ + private String name; + + /** + * 角色编码 + */ + private String code; + + /** + * 显示顺序 + */ + private Integer sort; + + /** + * 角色状态(1-正常 0-停用) + */ + private Integer status; + + /** + * 数据权限 + */ + private Integer dataScope; + + /** + * 创建人 ID + */ + private Long createBy; + + /** + * 更新人 ID + */ + private Long updateBy; + + /** + * 是否删除(0-否 1-是) + */ + private Integer isDeleted; +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/RoleMenu.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/RoleMenu.java new file mode 100644 index 0000000000000000000000000000000000000000..ec2878ff78b4d2387dd4833a869c6103681aa735 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/RoleMenu.java @@ -0,0 +1,32 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 角色和菜单关联表 + * @Version: 1.0 + */ +@TableName("sys_role_menu") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class RoleMenu { + /** + * 角色ID + */ + private Long roleId; + + /** + * 菜单ID + */ + private Long menuId; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/User.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/User.java new file mode 100644 index 0000000000000000000000000000000000000000..1cd9c214363426a437ba060f19871b7ad9ad2622 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/User.java @@ -0,0 +1,83 @@ +package tech.hypersense.system.model.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-25 + * @Description: 用户实体 + * @Version: 1.0 + */ +@TableName("sys_user") +@Getter +@Setter +public class User extends BaseEntity { + + /** + * 用户名 + */ + private String username; + + /** + * 昵称 + */ + private String nickname; + + /** + * 性别((1-男 2-女 0-保密) + */ + private Integer gender; + + /** + * 密码 + */ + private String password; + + /** + * 部门ID + */ + private Long deptId; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 联系方式 + */ + private String mobile; + + /** + * 状态((1-正常 0-禁用) + */ + private Integer status; + + /** + * 用户邮箱 + */ + private String email; + + /** + * 创建人 ID + */ + private Long createBy; + + /** + * 更新人 ID + */ + private Long updateBy; + + /** + * 是否删除(0-否 1-是) + */ + private Integer isDeleted; + + /** + * 微信 OpenID + */ + private String openid; +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/UserNotice.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/UserNotice.java new file mode 100644 index 0000000000000000000000000000000000000000..86aeb9efd399b1fd8a83e09b30e63d001b280d6d --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/UserNotice.java @@ -0,0 +1,53 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BaseEntity; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户通知公告实体对象 + * @Version: 1.0 + */ +@Getter +@Setter +@TableName("sys_user_notice") +public class UserNotice extends BaseEntity { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 公共通知id + */ + private Long noticeId; + /** + * 用户id + */ + private Long userId; + /** + * 读取状态,0未读,1已读 + */ + private Integer isRead; + /** + * 用户阅读时间 + */ + private LocalDateTime readTime; + + /** + * 逻辑删除标识(0-未删除 1-已删除) + */ + @TableLogic(value = "0", delval = "1") + private Integer isDeleted; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/UserRole.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/UserRole.java new file mode 100644 index 0000000000000000000000000000000000000000..0efec7f1c83df08de89df99f2c477ae51744e150 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/entity/UserRole.java @@ -0,0 +1,32 @@ +package tech.hypersense.system.model.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户和角色关联表 + * @Version: 1.0 + */ +@TableName("sys_user_role") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserRole { + /** + * 用户ID + */ + private Long userId; + + /** + * 角色ID + */ + private Long roleId; + + @TableField(exist = false) + private static final long serialVersionUID = 1L; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/ConfigForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/ConfigForm.java new file mode 100644 index 0000000000000000000000000000000000000000..db4abcd8a180bb4612a3b3dc5a0ef77adc6ddb9a --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/ConfigForm.java @@ -0,0 +1,41 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 系统配置 表单实体 + * @Version: 1.0 + */ +@Data +@Schema(description = "系统配置Form实体") +public class ConfigForm implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "主键") + private Long id; + + @NotBlank(message = "配置名称不能为空") + @Schema(description = "配置名称") + private String configName; + + @NotBlank(message = "配置键不能为空") + @Schema(description = "配置键") + private String configKey; + + @NotBlank(message = "配置值不能为空") + @Schema(description = "配置值") + private String configValue; + + @Schema(description = "描述、备注") + private String remark; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DeptForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DeptForm.java new file mode 100644 index 0000000000000000000000000000000000000000..98552268ba0f994bc3d59de1a6b5e115d4e21ee4 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DeptForm.java @@ -0,0 +1,40 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.validator.constraints.Range; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 部门表单对象 + * @Version: 1.0 + */ +@Schema(description = "部门表单对象") +@Getter +@Setter +public class DeptForm { + + @Schema(description="部门ID", example = "1001") + private Long id; + + @Schema(description="部门名称", example = "研发部") + private String name; + + @Schema(description="部门编号", example = "RD001") + private String code; + + @Schema(description="父部门ID", example = "1000") + @NotNull(message = "父部门ID不能为空") + private Long parentId; + + @Schema(description="状态(1:启用;0:禁用)", example = "1") + @Range(min = 0, max = 1, message = "状态值不正确") + private Integer status; + + @Schema(description="排序(数字越小排名越靠前)", example = "1") + private Integer sort; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DictDataForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DictDataForm.java new file mode 100644 index 0000000000000000000000000000000000000000..67dec0bdf94e016f8d15e3ad1365c686b98cdfa8 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DictDataForm.java @@ -0,0 +1,38 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典数据表单对象 + * @Version: 1.0 + */ +@Schema(description = "字典数据表单") +@Data +public class DictDataForm { + + @Schema(description = "字典ID") + private Long id; + + @Schema(description = "字典编码") + private String dictCode; + + @Schema(description = "字典值") + private String value; + + @Schema(description = "字典标签") + private String label; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "状态(1-启用,0-禁用)") + private Integer status; + + @Schema(description = "字典类型") + private String tagType; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DictForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DictForm.java new file mode 100644 index 0000000000000000000000000000000000000000..9c918d9eeb44aafa09b259c1f1c7e9316b93692c --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/DictForm.java @@ -0,0 +1,33 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典表单对象 + * @Version: 1.0 + */ +@Schema(description = "字典") +@Data +public class DictForm { + + @Schema(description = "字典ID",example = "1") + private Long id; + + @Schema(description = "字典名称",example = "性别") + private String name; + + @Schema(description = "字典编码", example ="gender") + private String dictCode; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "字典状态(1-启用,0-禁用)", example = "1") + @Range(min = 0, max = 1, message = "字典状态不正确") + private Integer status; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/EmailUpdateForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/EmailUpdateForm.java new file mode 100644 index 0000000000000000000000000000000000000000..eaf3fadb4090b0e0c91acdffc77c379630240e6f --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/EmailUpdateForm.java @@ -0,0 +1,25 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 修改邮箱表单 + * @Version: 1.0 + */ +@Schema(description = "修改邮箱表单") +@Data +public class EmailUpdateForm { + + @Schema(description = "邮箱") + @NotBlank(message = "邮箱不能为空") + private String email; + + @Schema(description = "验证码") + @NotBlank(message = "验证码不能为空") + private String code; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/MenuForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/MenuForm.java new file mode 100644 index 0000000000000000000000000000000000000000..318602bd4fd100760fcd81429e598c68eea21422 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/MenuForm.java @@ -0,0 +1,67 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Range; +import tech.hypersense.common.core.domain.model.KValue; +import tech.hypersense.system.enums.MenuTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 菜单表单对象 + * @Version: 1.0 + */ +@Schema(description = "菜单表单对象") +@Data +public class MenuForm { + + @Schema(description = "菜单ID") + private Long id; + + @Schema(description = "父菜单ID") + private Long parentId; + + @Schema(description = "菜单名称") + private String name; + + @Schema(description = "菜单类型(1-菜单 2-目录 3-外链 4-按钮)") + private MenuTypeEnum type; + + @Schema(description = "路由名称") + private String routeName; + + @Schema(description = "路由路径") + private String routePath; + + @Schema(description = "组件路径(vue页面完整路径,省略.vue后缀)") + private String component; + + @Schema(description = "权限标识") + private String perm; + + @Schema(description = "显示状态(1:显示;0:隐藏)") + @Range(max = 1, min = 0, message = "显示状态不正确") + private Integer visible; + + @Schema(description = "排序(数字越小排名越靠前)") + private Integer sort; + + @Schema(description = "菜单图标") + private String icon; + + @Schema(description = "跳转路径") + private String redirect; + + @Schema(description = "【菜单】是否开启页面缓存", example = "1") + private Integer keepAlive; + + @Schema(description = "【目录】只有一个子路由是否始终显示", example = "1") + private Integer alwaysShow; + + @Schema(description = "路由参数") + private List params; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/MobileUpdateForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/MobileUpdateForm.java new file mode 100644 index 0000000000000000000000000000000000000000..05b1073ac75e124ecd99bb131d3df0f75c8e3f56 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/MobileUpdateForm.java @@ -0,0 +1,25 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 修改手机表单 + * @Version: 1.0 + */ +@Schema(description = "修改手机表单") +@Data +public class MobileUpdateForm { + + @Schema(description = "手机号码") + @NotBlank(message = "手机号码不能为空") + private String mobile; + + @Schema(description = "验证码") + @NotBlank(message = "验证码不能为空") + private String code; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/NoticeForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/NoticeForm.java new file mode 100644 index 0000000000000000000000000000000000000000..6387df01b388ce95f82af71ce41ee5459cfc3489 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/NoticeForm.java @@ -0,0 +1,55 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.validator.constraints.Range; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知公告表单对象 + * @Version: 1.0 + */ +@Getter +@Setter +@Schema(description = "通知公告表单对象") +public class NoticeForm implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "通知ID") + private Long id; + + @Schema(description = "通知标题") + @NotBlank(message = "通知标题不能为空") + @Size(max=50, message="通知标题长度不能超过50个字符") + private String title; + + @Schema(description = "通知内容") + @NotBlank(message = "通知内容不能为空") + @Size(max=65535, message="通知内容长度不能超过65535个字符") + private String content; + + @Schema(description = "通知类型") + private Integer type; + + @Schema(description = "优先级(L-低 M-中 H-高)") + private String level; + + @Schema(description = "目标类型(1-全体 2-指定)") + @Range(min = 1, max = 2, message = "目标类型取值范围[1,2]") + private Integer targetType; + + @Schema(description = "接收人ID集合") + private List targetUserIds; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/PasswordUpdateForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/PasswordUpdateForm.java new file mode 100644 index 0000000000000000000000000000000000000000..fc2bdb0a897ab182d8dde0a28dab48f73c4f0e35 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/PasswordUpdateForm.java @@ -0,0 +1,22 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 修改密码表单 + * @Version: 1.0 + */ +@Schema(description = "修改密码表单") +@Data +public class PasswordUpdateForm { + + @Schema(description = "原密码") + private String oldPassword; + + @Schema(description = "新密码") + private String newPassword; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/RoleForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/RoleForm.java new file mode 100644 index 0000000000000000000000000000000000000000..dfb54d0e5f4923bb8f31ce3bf0c4ec111421b3f0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/RoleForm.java @@ -0,0 +1,39 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@Schema(description = "角色表单对象") +@Data +public class RoleForm { + + @Schema(description="角色ID") + private Long id; + + @Schema(description="角色名称") + @NotBlank(message = "角色名称不能为空") + private String name; + + @Schema(description="角色编码") + @NotBlank(message = "角色编码不能为空") + private String code; + + @Schema(description="排序") + private Integer sort; + + @Schema(description="角色状态(1-正常;0-停用)") + @Range(max = 1, min = 0, message = "角色状态不正确") + private Integer status; + + @Schema(description="数据权限") + private Integer dataScope; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/UserForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/UserForm.java new file mode 100644 index 0000000000000000000000000000000000000000..8de9378d0dc8ff84c03e1d7af17c6fbd3f41590e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/UserForm.java @@ -0,0 +1,62 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户表单对象 + * @Version: 1.0 + */ +@Schema(description = "用户表单对象") +@Data +public class UserForm { + + @Schema(description="用户ID") + private Long id; + + @Schema(description="用户名") + @NotBlank(message = "用户名不能为空") + private String username; + + @Schema(description="昵称") + @NotBlank(message = "昵称不能为空") + private String nickname; + + + @Schema(description="手机号码") + @Pattern(regexp = "^$|^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$", message = "手机号码格式不正确") + private String mobile; + + @Schema(description="性别") + private Integer gender; + + @Schema(description="用户头像") + private String avatar; + + @Schema(description="邮箱") + private String email; + + @Schema(description="用户状态(1:正常;0:禁用)") + @Range(min = 0, max = 1, message = "用户状态不正确") + private Integer status; + + @Schema(description="部门ID") + private Long deptId; + + @Schema(description="角色ID集合") + @NotEmpty(message = "用户角色不能为空") + private List roleIds; + + @Schema(description="微信openId") + private String openId; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/UserProfileForm.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/UserProfileForm.java new file mode 100644 index 0000000000000000000000000000000000000000..d3c677848e2e6dd5158479d6d4bc719298d735fe --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/form/UserProfileForm.java @@ -0,0 +1,38 @@ +package tech.hypersense.system.model.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 个人中心用户信息 + * @Version: 1.0 + */ +@Schema(description = "个人中心用户信息") +@Data +public class UserProfileForm { + + @Schema(description = "用户ID") + private Long id; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "用户昵称") + private String nickname; + + @Schema(description = "头像URL") + private String avatar; + + @Schema(description = "性别") + private Integer gender; + + @Schema(description = "手机号") + private String mobile; + + @Schema(description = "邮箱") + private String email; + + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/ArchivesQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/ArchivesQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..453910bea52690d3c189270ac9adb6d1e1317065 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/ArchivesQuery.java @@ -0,0 +1,26 @@ +package tech.hypersense.system.model.query; + +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-25 + * @Description: 博客文章分页查询对象 + * @Version: 1.0 + */ +@Schema(description ="博客文章查询对象") +@Getter +@Setter +public class ArchivesQuery extends BasePageQuery { + + @Schema(description = "文章类型(字典code:archives_type)") + private String type; + @Schema(description = "发布时间") + private List publishTime; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/ConfigPageQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/ConfigPageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..98cad26bbd763347d9a19f3f6517bb26f9a50c43 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/ConfigPageQuery.java @@ -0,0 +1,21 @@ +package tech.hypersense.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 系统配置查询对象 + * @Version: 1.0 + */ +@Getter +@Setter +@Schema(description = "系统配置分页查询") +public class ConfigPageQuery extends BasePageQuery { + + @Schema(description="关键字(配置项名称/配置项值)") + private String keywords; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DeptQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DeptQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..a25de37bbc2020ddcc841716763ee858ed790bd9 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DeptQuery.java @@ -0,0 +1,22 @@ +package tech.hypersense.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 部门查询对象 + * @Version: 1.0 + */ +@Schema(description ="部门分页查询对象") +@Data +public class DeptQuery { + + @Schema(description="关键字(部门名称)") + private String keywords; + + @Schema(description="状态(1->正常;0->禁用)") + private Integer status; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DictDataPageQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DictDataPageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..05fc92cc4fee8cc762a535c2fbde41db0e2344a7 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DictDataPageQuery.java @@ -0,0 +1,24 @@ +package tech.hypersense.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: + * @Version: 1.0 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description ="字典数据分页查询对象") +public class DictDataPageQuery extends BasePageQuery { + + @Schema(description="关键字(字典数据标签/值)") + private String keywords; + + @Schema(description="字典编码") + private String dictCode; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DictPageQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DictPageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..d2497496216ee36fc8bc168cc3232ec5e7ab03a9 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/DictPageQuery.java @@ -0,0 +1,25 @@ +package tech.hypersense.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典数据项分页查询对象 + * @Version: 1.0 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description ="字典数据项分页查询对象") +public class DictPageQuery extends BasePageQuery { + + @Schema(description="关键字(字典名称)") + private String keywords; + + @Schema(description="字典编码") + private String typeCode; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/LogPageQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/LogPageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..c26a127ad61ad93b214a9f9c0e57d35ed3b600ea --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/LogPageQuery.java @@ -0,0 +1,27 @@ +package tech.hypersense.system.model.query; + +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-25 + * @Description: 日志分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "日志分页查询对象") +@Getter +@Setter +public class LogPageQuery extends BasePageQuery { + + @Schema(description="关键字(日志内容/请求路径/请求方法/地区/浏览器/终端系统)") + private String keywords; + + @Schema(description="操作时间范围") + List createTime; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/MenuQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/MenuQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..5676c3d5dba891f1ff03860ec006ba5fc7487a8b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/MenuQuery.java @@ -0,0 +1,22 @@ +package tech.hypersense.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 菜单查询对象 + * @Version: 1.0 + */ +@Schema(description ="菜单查询对象") +@Data +public class MenuQuery { + + @Schema(description="关键字(菜单名称)") + private String keywords; + + @Schema(description="状态(1->显示;0->隐藏)") + private Integer status; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/NoticePageQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/NoticePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..c6d866cbd1dad16388faecfc8a8d77261a57dfcf --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/NoticePageQuery.java @@ -0,0 +1,36 @@ +package tech.hypersense.system.model.query; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知公告分页查询对象 + * @Version: 1.0 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description ="通知公告查询对象") +public class NoticePageQuery extends BasePageQuery { + + @Schema(description = "通知标题") + private String title; + + @Schema(description = "发布状态(0-未发布 1已发布 -1已撤回)") + private Integer publishStatus; + + @Schema(description = "发布时间(起止)") + private List publishTime; + + @Schema(description = "查询人ID") + private Long userId; + + @Schema(description = "是否已读(0-未读 1-已读)") + private Integer isRead; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/RolePageQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/RolePageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..27bb0612efdec7da0ea26ecff752fd77c4cdbbfd --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/RolePageQuery.java @@ -0,0 +1,32 @@ +package tech.hypersense.system.model.query; + +import com.fasterxml.jackson.annotation.JsonFormat; +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.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 角色分页查询对象 + * @Version: 1.0 + */ +@Schema(description = "角色分页查询对象") +@Getter +@Setter +public class RolePageQuery extends BasePageQuery { + + @Schema(description="关键字(角色名称/角色编码)") + private String keywords; + + @Schema(description="开始日期") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDateTime startDate; + + @Schema(description="结束日期") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDateTime endDate; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/UserPageQuery.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/UserPageQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..d0d5f9b42fd542facc5478acff9407e5ac8d528d --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/query/UserPageQuery.java @@ -0,0 +1,47 @@ +package tech.hypersense.system.model.query; + +import cn.hutool.db.sql.Direction; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import tech.hypersense.common.core.annotation.ValidField; +import tech.hypersense.common.core.domain.model.base.BasePageQuery; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户分页查询对象 + * @Version: 1.0 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Schema(description = "用户分页查询对象") +public class UserPageQuery extends BasePageQuery { + + @Schema(description = "关键字(用户名/昵称/手机号)") + private String keywords; + + @Schema(description = "用户状态") + private Integer status; + + @Schema(description = "部门ID") + private Long deptId; + + @Schema(description = "角色ID") + private List roleIds; + + @Schema(description = "创建时间范围") + private List createTime; + + @Schema(description = "排序的字段") + @ValidField(allowedValues = {"create_time","update_time"}) + private String field; + + @Schema(description = "排序方式(正序:ASC;反序:DESC)") + private Direction direction; + + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/ConfigVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/ConfigVO.java new file mode 100644 index 0000000000000000000000000000000000000000..ffbbf34eabfad10d4276bcba4b0781bedff0a911 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/ConfigVO.java @@ -0,0 +1,35 @@ +package tech.hypersense.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 系统配置视图对象 + * @Version: 1.0 + */ +@Data +@Builder +@EqualsAndHashCode(callSuper = false) +@Schema(description = "系统配置VO") +public class ConfigVO { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "配置名称") + private String configName; + + @Schema(description = "配置键") + private String configKey; + + @Schema(description = "配置值") + private String configValue; + + @Schema(description = "描述、备注") + private String remark; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DeptVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DeptVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d5e16b8cea7e7f9892238b524299b915375eb270 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DeptVO.java @@ -0,0 +1,49 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 部门视图对象 + * @Version: 1.0 + */ +@Schema(description = "部门视图对象") +@Data +public class DeptVO { + + @Schema(description = "部门ID") + private Long id; + + @Schema(description = "父部门ID") + private Long parentId; + + @Schema(description = "部门名称") + private String name; + + @Schema(description = "部门编号") + private String code; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "状态(1:启用;0:禁用)") + private Integer status; + + @Schema(description = "子部门") + private List children; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime createTime; + @Schema(description = "修改时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime updateTime; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictDataPageVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictDataPageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..59ffbe1a2f858b05208e8fbef8492a05cf4d58ed --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictDataPageVO.java @@ -0,0 +1,40 @@ +package tech.hypersense.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典数据项分页VO + * @Version: 1.0 + */ +@Schema(description = "字典数据分页对象") +@Getter +@Setter +public class DictDataPageVO { + + @Schema(description = "字典数据ID") + private Long id; + + @Schema(description = "字典编码") + private String dictCode; + + @Schema(description = "字典标签") + private String label; + + @Schema(description = "字典值") + private String value; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "状态(1:启用,0:禁用)") + private Integer status; + + @Schema(description = "备注") + private String remark; + + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictPageVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictPageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..8b02b1e8d3f5a4b5bf2ebe17ead6a9f8800868af --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictPageVO.java @@ -0,0 +1,30 @@ +package tech.hypersense.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典分页VO + * @Version: 1.0 + */ +@Schema(description = "字典分页对象") +@Getter +@Setter +public class DictPageVO { + + @Schema(description = "字典ID") + private Long id; + + @Schema(description = "字典名称") + private String name; + + @Schema(description = "字典编码") + private String dictCode; + + @Schema(description = "字典状态(1-启用,0-禁用)") + private Integer status; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictVO.java new file mode 100644 index 0000000000000000000000000000000000000000..784ea8361a3e8b5a27d3c9b7aaaf0a0cc64d980c --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/DictVO.java @@ -0,0 +1,45 @@ +package tech.hypersense.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 字典数据项分页VO + * @Version: 1.0 + */ +@Schema(description = "字典数据分页对象") +@Getter +@Setter +public class DictVO { + + @Schema(description = "字典名称") + private String name; + + @Schema(description = "字典编码") + private String dictCode; + + @Schema(description = "字典数据集合") + private List dictDataList; + + @Schema(description = "字典数据") + @Getter + @Setter + public static class DictData { + + @Schema(description = "字典数据值") + private String value; + + @Schema(description = "字典数据标签") + private String label; + + @Schema(description = "标签类型") + private String tagType; + } + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/LogPageVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/LogPageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..ccaff8638317e1d272077852a46c8eeccda6f63b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/LogPageVO.java @@ -0,0 +1,59 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.common.core.enums.LogModuleEnum; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 系统日志分页VO + * @Version: 1.0 + */ +@Data +@Schema(description = "系统日志分页VO") +public class LogPageVO implements Serializable { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "日志模块") + private LogModuleEnum module; + + @Schema(description = "日志内容") + private String content; + + @Schema(description = "请求路径") + private String requestUri; + + @Schema(description = "请求方法") + private String method; + + @Schema(description = "IP 地址") + private String ip; + + @Schema(description = "地区") + private String region; + + @Schema(description = "浏览器") + private String browser; + + @Schema(description = "终端系统") + private String os; + + @Schema(description = "执行时间(毫秒)") + private Long executionTime; + + @Schema(description = "创建人ID") + private Long createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @Schema(description = "操作人") + private String operator; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/MenuVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/MenuVO.java new file mode 100644 index 0000000000000000000000000000000000000000..2590ba84da2b0d274277a5afb2ceb6efe39464d0 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/MenuVO.java @@ -0,0 +1,60 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import tech.hypersense.system.enums.MenuTypeEnum; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 菜单视图对象 + * @Version: 1.0 + */ +@Schema(description ="菜单视图对象") +@Data +public class MenuVO { + + @Schema(description = "菜单ID") + private Long id; + + @Schema(description = "父菜单ID") + private Long parentId; + + @Schema(description = "菜单名称") + private String name; + + @Schema(description="菜单类型") + private MenuTypeEnum type; + + @Schema(description = "路由名称") + private String routeName; + + @Schema(description = "路由路径") + private String routePath; + + @Schema(description = "组件路径") + private String component; + + @Schema(description = "菜单排序(数字越小排名越靠前)") + private Integer sort; + + @Schema(description = "菜单是否可见(1:显示;0:隐藏)") + private Integer visible; + + @Schema(description = "ICON") + private String icon; + + @Schema(description = "跳转路径") + private String redirect; + + @Schema(description="按钮权限标识") + private String perm; + + @Schema(description = "子菜单") + @JsonInclude(value = JsonInclude.Include.NON_NULL) + private List children; + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/NoticeDetailVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/NoticeDetailVO.java new file mode 100644 index 0000000000000000000000000000000000000000..0ac7f602f673e5bdaf574280e021ac537e30fdcc --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/NoticeDetailVO.java @@ -0,0 +1,43 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 阅读通知公告VO + * @Version: 1.0 + */ +@Data +public class NoticeDetailVO { + + @Schema(description = "通知ID") + private Long id; + + @Schema(description = "通知标题") + private String title; + + @Schema(description = "通知内容") + private String content; + + @Schema(description = "通知类型") + private Integer type; + + @Schema(description = "发布人") + private String publisherName; + + @Schema(description = "优先级(L-低 M-中 H-高)") + private String level; + + @Schema(description = "发布状态(0-未发布 1已发布 2已撤回) 冗余字段,方便判断是否已经发布") + private Integer publishStatus; + + @Schema(description = "发布时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime publishTime; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/NoticePageVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/NoticePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..c7d5060eb7bff65cc98d4153cb32cdc0aebb9828 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/NoticePageVO.java @@ -0,0 +1,61 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 通知公告视图对象 + * @Version: 1.0 + */ +@Getter +@Setter +@Schema(description = "通知公告视图对象") +public class NoticePageVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "通知ID") + private Long id; + + @Schema(description = "通知标题") + private String title; + + @Schema(description = "通知状态") + private Integer publishStatus; + + @Schema(description = "通知类型") + private Integer type; + + @Schema(description = "发布人姓名") + private String publisherName; + + @Schema(description = "通知等级") + private String level; + + @Schema(description = "发布时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime publishTime; + + @Schema(description = "是否已读") + private Integer isRead; + + @Schema(description = "目标类型") + private Integer targetType; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime createTime; + + @Schema(description = "撤回时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime revokeTime; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/RolePageVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/RolePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..e2f2bff46ff3eb90bae332bec93ea72b784c6c22 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/RolePageVO.java @@ -0,0 +1,39 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 角色分页对象 + * @Version: 1.0 + */ +@Schema(description ="角色分页对象") +@Data +public class RolePageVO { + + @Schema(description="角色ID") + private Long id; + + @Schema(description="角色名称") + private String name; + + @Schema(description="角色编码") + private String code; + + @Schema(description="角色状态") + private Integer status; + + @Schema(description="排序") + private Integer sort; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime updateTime; +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/RouteVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/RouteVO.java new file mode 100644 index 0000000000000000000000000000000000000000..631a4c030771287c3eaa05aecea36132c81fde2f --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/RouteVO.java @@ -0,0 +1,64 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 路由对象 + * @Version: 1.0 + */ +@Schema(description = "路由对象") +@Data +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class RouteVO { + + @Schema(description = "路由路径", example = "user") + private String path; + + @Schema(description = "组件路径", example = "system/user/index") + private String component; + + @Schema(description = "跳转链接", example = "https://www.youlai.tech") + private String redirect; + + @Schema(description = "路由名称") + private String name; + + @Schema(description = "路由属性") + private Meta meta; + + @Schema(description = "路由属性类型") + @Data + public static class Meta { + + @Schema(description = "路由title") + private String title; + + @Schema(description = "ICON") + private String icon; + + @Schema(description = "是否隐藏(true-是 false-否)", example = "true") + private Boolean hidden; + + @Schema(description = "【菜单】是否开启页面缓存", example = "true") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean keepAlive; + + @Schema(description = "【目录】只有一个子路由是否始终显示", example = "true") + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean alwaysShow; + + @Schema(description = "路由参数") + private Map params; + } + + @Schema(description = "子路由列表") + private List children; +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserInfoVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserInfoVO.java new file mode 100644 index 0000000000000000000000000000000000000000..7d5cad7e557b9d25cb54258594c2e5f405b18237 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserInfoVO.java @@ -0,0 +1,36 @@ +package tech.hypersense.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 当前登录用户视图对象 + * @Version: 1.0 + */ +@Schema(description ="当前登录用户视图对象") +@Data +public class UserInfoVO { + + @Schema(description="用户ID") + private Long userId; + + @Schema(description="用户名") + private String username; + + @Schema(description="用户昵称") + private String nickname; + + @Schema(description="头像地址") + private String avatar; + + @Schema(description="用户角色编码集合") + private Set roles; + + @Schema(description="用户权限标识集合") + private Set perms; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserNoticePageVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserNoticePageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..c6d7f2a1cde0320bfcefbb7818d87b98135311b7 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserNoticePageVO.java @@ -0,0 +1,42 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户公告VO + * @Version: 1.0 + */ +@Data +@Schema(description = "用户公告VO") +public class UserNoticePageVO { + + @Schema(description = "通知ID") + private Long id; + + @Schema(description = "通知标题") + private String title; + + @Schema(description = "通知类型") + private Integer type; + + @Schema(description = "通知等级") + private String level; + + @Schema(description = "发布人姓名") + private String publisherName; + + @Schema(description = "发布时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + private LocalDateTime publishTime; + + @Schema(description = "是否已读") + private Integer isRead; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserPageVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserPageVO.java new file mode 100644 index 0000000000000000000000000000000000000000..476282df7597c4de80fffbb517d801ca95719242 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserPageVO.java @@ -0,0 +1,54 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 用户分页对象 + * @Version: 1.0 + */ +@Schema(description ="用户分页对象") +@Data +public class UserPageVO { + + @Schema(description="用户ID") + private Long id; + + @Schema(description="用户名") + private String username; + + @Schema(description="用户昵称") + private String nickname; + + @Schema(description="手机号") + private String mobile; + + @Schema(description="性别") + private Integer gender; + + @Schema(description="用户头像地址") + private String avatar; + + @Schema(description="用户邮箱") + private String email; + + @Schema(description="用户状态(1:启用;0:禁用)") + private Integer status; + + @Schema(description="部门名称") + private String deptName; + + @Schema(description="角色名称,多个使用英文逗号(,)分割") + private String roleNames; + + @Schema(description="创建时间") + @JsonFormat(pattern = "yyyy/MM/dd HH:mm") + private LocalDateTime createTime; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserProfileVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserProfileVO.java new file mode 100644 index 0000000000000000000000000000000000000000..a2ec8ae35e8eb1ca48bca3dcca9385707375aae1 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/UserProfileVO.java @@ -0,0 +1,50 @@ +package tech.hypersense.system.model.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 个人中心用户信息 + * @Version: 1.0 + */ +@Schema(description = "个人中心用户信息") +@Data +public class UserProfileVO { + + @Schema(description = "用户ID") + private Long id; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "用户昵称") + private String nickname; + + @Schema(description = "头像URL") + private String avatar; + + @Schema(description = "性别") + private Integer gender; + + @Schema(description = "手机号") + private String mobile; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "部门名称") + private String deptName; + + @Schema(description = "角色名称") + private String roleNames; + + @Schema(description = "创建时间") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date createTime; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/VisitStatsVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/VisitStatsVO.java new file mode 100644 index 0000000000000000000000000000000000000000..ef592fb251973dd8433477f9f90f10bb96b9b6f7 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/VisitStatsVO.java @@ -0,0 +1,39 @@ +package tech.hypersense.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 访问量统计视图对象 + * @Version: 1.0 + */ +@Schema(description = "访问量统计视图对象") +@Getter +@Setter +public class VisitStatsVO { + + @Schema(description = "今日独立访客数 (UV)") + private Integer todayUvCount; + + @Schema(description = "累计独立访客数 (UV)") + private Integer totalUvCount; + + @Schema(description = "独立访客增长率") + private BigDecimal uvGrowthRate; + + @Schema(description = "今日页面浏览量 (PV)") + private Integer todayPvCount; + + @Schema(description = "累计页面浏览量 (PV)") + private Integer totalPvCount; + + @Schema(description = "页面浏览量增长率") + private BigDecimal pvGrowthRate; + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/VisitTrendVO.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/VisitTrendVO.java new file mode 100644 index 0000000000000000000000000000000000000000..d33ce37ab40de23c9ab2e0572b847f68959edd95 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/model/vo/VisitTrendVO.java @@ -0,0 +1,29 @@ +package tech.hypersense.system.model.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-25 + * @Description: 访问趋势VO + * @Version: 1.0 + */ +@Schema(description = "访问趋势VO") +@Getter +@Setter +public class VisitTrendVO { + + @Schema(description = "日期列表") + private List dates; + + @Schema(description = "浏览量(PV)") + private List pvList; + + @Schema(description = "IP数") + private List ipList; + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/ConfigService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/ConfigService.java new file mode 100644 index 0000000000000000000000000000000000000000..556889e4a21d42379ea81d4dda4425f48afea8aa --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/ConfigService.java @@ -0,0 +1,68 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.system.model.entity.Config; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.query.ConfigPageQuery; +import tech.hypersense.system.model.vo.ConfigVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统配置Service接口 + * @Version: 1.0 + */ +public interface ConfigService extends IService { + + /** + * 分页查询系统配置 + * @param sysConfigPageQuery 查询参数 + * @return 系统配置分页列表 + */ + IPage page(ConfigPageQuery sysConfigPageQuery); + + /** + * 保存系统配置 + * @param sysConfigForm 系统配置表单 + * @return 是否保存成功 + */ + boolean save(ConfigForm sysConfigForm); + + /** + * 获取系统配置表单数据 + * + * @param id 系统配置ID + * @return 系统配置表单数据 + */ + ConfigForm getConfigFormData(Long id); + + /** + * 编辑系统配置 + * @param id 系统配置ID + * @param sysConfigForm 系统配置表单 + * @return 是否编辑成功 + */ + boolean edit(Long id, ConfigForm sysConfigForm); + + /** + * 删除系统配置 + * @param ids 系统配置ID + * @return 是否删除成功 + */ + boolean delete(Long ids); + + /** + * 刷新系统配置缓存 + * @return 是否刷新成功 + */ + boolean refreshCache(); + + /** + * 获取系统配置 + * @param key 配置键 + * @return 配置值 + */ + Object getSystemConfig(String key); + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DeptService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DeptService.java new file mode 100644 index 0000000000000000000000000000000000000000..22afa7a187f3f5f7d07836fab91c8a9a9fa37539 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DeptService.java @@ -0,0 +1,65 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.system.model.entity.Dept; +import tech.hypersense.system.model.form.DeptForm; +import tech.hypersense.system.model.query.DeptQuery; +import tech.hypersense.system.model.vo.DeptVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 部门业务接口 + * @Version: 1.0 + */ +public interface DeptService extends IService { + /** + * 部门列表 + * + * @return 部门列表 + */ + List getDeptList(DeptQuery queryParams); + + /** + * 部门树形下拉选项 + * + * @return 部门树形下拉选项 + */ + List> listDeptOptions(); + + /** + * 新增部门 + * + * @param formData 部门表单 + * @return 部门ID + */ + Long saveDept(DeptForm formData); + + /** + * 修改部门 + * + * @param deptId 部门ID + * @param formData 部门表单 + * @return 部门ID + */ + Long updateDept(Long deptId, DeptForm formData); + + /** + * 删除部门 + * + * @param ids 部门ID,多个以英文逗号,拼接字符串 + * @return 是否成功 + */ + boolean deleteByIds(String ids); + + /** + * 获取部门详情 + * + * @param deptId 部门ID + * @return 部门详情 + */ + DeptForm getDeptForm(Long deptId); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DictDataService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DictDataService.java new file mode 100644 index 0000000000000000000000000000000000000000..656e1e26596b832772e5b7cc4e87995b32d8e8d1 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DictDataService.java @@ -0,0 +1,68 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.system.model.entity.DictData; +import tech.hypersense.system.model.form.DictDataForm; +import tech.hypersense.system.model.query.DictDataPageQuery; +import tech.hypersense.system.model.vo.DictDataPageVO; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字典数据接口 + * @Version: 1.0 + */ +public interface DictDataService extends IService { + + /** + * 字典数据分页列表 + * + * @param queryParams + * @return + */ + Page getDictDataPage(DictDataPageQuery queryParams); + + /** + * 获取字典数据表单 + * + * @param id 字典数据ID + * @return + */ + DictDataForm getDictDataForm(Long id); + + /** + * 保存字典数据 + * + * @param formData + * @return + */ + boolean saveDictData(DictDataForm formData); + + /** + * 更新字典数据 + * + * @param formData 字典数据表单 + * @return + */ + boolean updateDictData(DictDataForm formData); + + /** + * 删除字典数据 + * + * @param ids 字典数据ID,多个逗号分隔 + */ + void deleteDictDataByIds(String ids); + + /** + * 获取字典数据列表 + * + * @param dictCode 字典编码 + * @return + */ + List> getDictDataList(String dictCode); +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DictService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DictService.java new file mode 100644 index 0000000000000000000000000000000000000000..ddacd2337b41933d5aebed9e4139e0241d5d7d1d --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/DictService.java @@ -0,0 +1,68 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.system.model.entity.Dict; +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 java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 字典业务接口 + * @Version: 1.0 + */ +public interface DictService extends IService { + + /** + * 字典分页列表 + * + * @param queryParams 分页查询对象 + * @return 字典分页列表 + */ + Page getDictPage(DictPageQuery queryParams); + + /** + * 获取字典表单详情 + * + * @param id 字典ID + * @return 字典表单 + */ + DictForm getDictForm(Long id); + + /** + * 新增字典 + * + * @param dictForm 字典表单 + * @return 是否成功 + */ + boolean saveDict(DictForm dictForm); + + /** + * 修改字典 + * + * @param id 字典ID + * @param dictForm 字典表单 + * @return 是否成功 + */ + boolean updateDict(Long id, DictForm dictForm); + + /** + * 删除字典 + * + * @param ids 字典ID集合 + */ + void deleteDictByIds(List ids); + + + /** + * 获取字典列表(包含字典数据) + * + * @return 字典列表 + */ + List getAllDictWithData(); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/LogService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/LogService.java new file mode 100644 index 0000000000000000000000000000000000000000..717ffae7f64e1f1469fecbf0182cfad5b936716d --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/LogService.java @@ -0,0 +1,41 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.system.model.entity.Log; +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 java.time.LocalDate; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 系统日志 服务接口 + * @Version: 1.0 + */ +public interface LogService extends IService { + + /** + * 获取日志分页列表 + */ + Page getLogPage(LogPageQuery queryParams); + + + /** + * 获取访问趋势 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + */ + VisitTrendVO getVisitTrend(LocalDate startDate, LocalDate endDate); + + /** + * 获取访问统计 + */ + VisitStatsVO getVisitStats(); + +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/MenuService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/MenuService.java new file mode 100644 index 0000000000000000000000000000000000000000..b993e97432eeacb39473d0f96d40a4817485da0a --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/MenuService.java @@ -0,0 +1,76 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.system.model.entity.Menu; +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 java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 菜单业务接口 + * @Version: 1.0 + */ +public interface MenuService extends IService { + + /** + * 获取菜单表格列表 + */ + List listMenus(MenuQuery queryParams); + + /** + * 获取菜单下拉列表 + * + * @param onlyParent 是否只查询父级菜单 + */ + List> listMenuOptions(boolean onlyParent); + + /** + * 新增菜单 + * + * @param menuForm 菜单表单对象 + */ + boolean saveMenu(MenuForm menuForm); + + /** + * 获取路由列表 + */ + List getCurrentUserRoutes(); + + /** + * 修改菜单显示状态 + * + * @param menuId 菜单ID + * @param visible 是否显示(1-显示 0-隐藏) + */ + boolean updateMenuVisible(Long menuId, Integer visible); + + /** + * 获取菜单表单数据 + * + * @param id 菜单ID + */ + MenuForm getMenuForm(Long id); + + /** + * 删除菜单 + * + * @param id 菜单ID + */ + boolean deleteMenu(Long id); + + /** + * 代码生成时添加菜单 + * + * @param parentMenuId 父菜单ID + * @param genConfig 实体名 + */ + void addMenuForCodegen(Long parentMenuId, GenConfig genConfig); +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/NoticeService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/NoticeService.java new file mode 100644 index 0000000000000000000000000000000000000000..5f8f340f55fb4eca674f2fc784e298b713dc8310 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/NoticeService.java @@ -0,0 +1,91 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.system.model.entity.Notice; +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; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 通知公告服务类 + * @Version: 1.0 + */ +public interface NoticeService extends IService { + + /** + * 通知公告分页列表 + * + * @return 通知公告分页列表 + */ + IPage getNoticePage(NoticePageQuery queryParams); + + /** + * 获取通知公告表单数据 + * + * @param id 通知公告ID + * @return 通知公告表单对象 + */ + NoticeForm getNoticeFormData(Long id); + + /** + * 新增通知公告 + * + * @param formData 通知公告表单对象 + * @return 是否新增成功 + */ + boolean saveNotice(NoticeForm formData); + + /** + * 修改通知公告 + * + * @param id 通知公告ID + * @param formData 通知公告表单对象 + * @return 是否修改成功 + */ + boolean updateNotice(Long id, NoticeForm formData); + + /** + * 删除通知公告 + * + * @param ids 通知公告ID,多个以英文逗号(,)分割 + * @return 是否删除成功 + */ + boolean deleteNotices(String ids); + + /** + * 发布通知公告 + * + * @param id 通知公告ID + * @return 是否发布成功 + */ + boolean publishNotice(Long id); + + /** + * 撤回通知公告 + * + * @param id 通知公告ID + * @return 是否撤回成功 + */ + boolean revokeNotice(Long id); + + /** + * 阅读获取通知公告详情 + * + * @param id 通知公告ID + * @return 通知公告详情 + */ + NoticeDetailVO getNoticeDetail(Long id); + + /** + * 获取我的通知公告分页列表 + * + * @param queryParams 查询参数 + * @return 通知公告分页列表 + */ + IPage getMyNoticePage(NoticePageQuery queryParams); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/RoleMenuService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/RoleMenuService.java new file mode 100644 index 0000000000000000000000000000000000000000..06db8f735cd27d8a78e13296526d00f97f48d36e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/RoleMenuService.java @@ -0,0 +1,53 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.system.model.entity.RoleMenu; + +import java.util.List; +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: + * @Version: 1.0 + */ +public interface RoleMenuService extends IService { + + /** + * 获取角色拥有的菜单ID集合 + * + * @param roleId 角色ID + * @return 菜单ID集合 + */ + List listMenuIdsByRoleId(Long roleId); + + + /** + * 刷新权限缓存(所有角色) + */ + void refreshRolePermsCache(); + + /** + * 刷新权限缓存(指定角色) + * + * @param roleCode 角色编码 + */ + void refreshRolePermsCache(String roleCode); + + /** + * 刷新权限缓存(修改角色编码时调用) + * + * @param oldRoleCode 旧角色编码 + * @param newRoleCode 新角色编码 + */ + void refreshRolePermsCache(String oldRoleCode, String newRoleCode); + + /** + * 获取角色权限集合 + * + * @param roles 角色编码集合 + * @return 权限集合 + */ + Set getRolePermsByRoleCodes(Set roles); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/RoleService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/RoleService.java new file mode 100644 index 0000000000000000000000000000000000000000..a735ef9f38f6fc125903ee0e1ba28232600c19b5 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/RoleService.java @@ -0,0 +1,94 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.system.model.entity.Role; +import tech.hypersense.system.model.form.RoleForm; +import tech.hypersense.system.model.query.RolePageQuery; +import tech.hypersense.system.model.vo.RolePageVO; + +import java.util.List; +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 角色业务接口层 + * @Version: 1.0 + */ +public interface RoleService extends IService { + + /** + * 角色分页列表 + * + * @param queryParams + * @return + */ + Page getRolePage(RolePageQuery queryParams); + + + /** + * 角色下拉列表 + * + * @return + */ + List> listRoleOptions(); + + /** + * + * @param roleForm + * @return + */ + boolean saveRole(RoleForm roleForm); + + /** + * 获取角色表单数据 + * + * @param roleId 角色ID + * @return {@link RoleForm} – 角色表单数据 + */ + RoleForm getRoleForm(Long roleId); + + /** + * 修改角色状态 + * + * @param roleId 角色ID + * @param status 角色状态(1:启用;0:禁用) + * @return {@link Boolean} + */ + boolean updateRoleStatus(Long roleId, Integer status); + + /** + * 批量删除角色 + * + * @param ids 角色ID,多个使用英文逗号(,)分割 + */ + void deleteRoles(String ids); + + /** + * 获取角色的菜单ID集合 + * + * @param roleId 角色ID + * @return 菜单ID集合(包括按钮权限ID) + */ + List getRoleMenuIds(Long roleId); + + /** + * 修改角色的资源权限 + * + * @param roleId 角色ID + * @param menuIds 菜单ID集合 + */ + void assignMenusToRole(Long roleId, List menuIds); + + /** + * 获取最大范围的数据权限 + * + * @param roles + * @return + */ + Integer getMaximumDataScope(Set roles); + + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserNoticeService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserNoticeService.java new file mode 100644 index 0000000000000000000000000000000000000000..ff878e33196cf692c13e0a4e22f86f4a4fad528b --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserNoticeService.java @@ -0,0 +1,33 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.system.model.entity.UserNotice; +import tech.hypersense.system.model.query.NoticePageQuery; +import tech.hypersense.system.model.vo.NoticePageVO; +import tech.hypersense.system.model.vo.UserNoticePageVO; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户公告状态服务类 + * @Version: 1.0 + */ +public interface UserNoticeService extends IService { + + /** + * 全部标记为已读 + * + * @return 是否成功 + */ + boolean readAll(); + + /** + * 分页获取我的通知公告 + * @param page 分页对象 + * @param queryParams 查询参数 + * @return 我的通知公告分页列表 + */ + IPage getMyNoticePage(Page page, NoticePageQuery queryParams); +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserRoleService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserRoleService.java new file mode 100644 index 0000000000000000000000000000000000000000..a5aead129f2363f3070474cb8177500c48ee8605 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserRoleService.java @@ -0,0 +1,32 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.system.model.entity.UserRole; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户角色关联操作 + * @Version: 1.0 + */ +public interface UserRoleService extends IService { + + /** + * 保存用户角色 + * + * @param userId + * @param roleIds + * @return + */ + boolean saveUserRoles(Long userId, List roleIds); + + /** + * 判断角色是否存在绑定的用户 + * + * @param roleId 角色ID + * @return true:已分配 false:未分配 + */ + boolean hasAssignedUsers(Long roleId); +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserService.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..8e7a7d220e1e624b930ed7656cfc9d224f51a497 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/UserService.java @@ -0,0 +1,190 @@ +package tech.hypersense.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.model.modules.system.dto.UserAuthInfo; +import tech.hypersense.system.model.dto.UserExportDTO; +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 java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-26 + * @Description: 用户业务接口 + * @Version: 1.0 + */ +public interface UserService extends IService { + + /** + * 用户分页列表 + * + * @return {@link IPage } 用户分页列表 + */ + IPage getUserPage(UserPageQuery queryParams); + + /** + * 获取用户表单数据 + * + * @param userId 用户ID + * @return {@link UserForm} 用户表单数据 + */ + UserForm getUserFormData(Long userId); + + + /** + * 新增用户 + * + * @param userForm 用户表单对象 + * @return {@link Boolean} 是否新增成功 + */ + boolean saveUser(UserForm userForm); + + /** + * 修改用户 + * + * @param userId 用户ID + * @param userForm 用户表单对象 + * @return {@link Boolean} 是否修改成功 + */ + boolean updateUser(Long userId, UserForm userForm); + + + /** + * 删除用户 + * + * @param idsStr 用户ID,多个以英文逗号(,)分割 + * @return {@link Boolean} 是否删除成功 + */ + boolean deleteUsers(String idsStr); + + + /** + * 根据用户名获取认证信息 + * + * @param username 用户名 + * @return {@link UserAuthInfo} + */ + + UserAuthInfo getUserAuthInfo(String username); + + + /** + * 获取导出用户列表 + * + * @param queryParams 查询参数 + * @return {@link List } 导出用户列表 + */ + List listExportUsers(UserPageQuery queryParams); + + + /** + * 获取登录用户信息 + * + * @return {@link UserInfoVO} 登录用户信息 + */ + UserInfoVO getCurrentUserInfo(); + + /** + * 获取个人中心用户信息 + * + * @return {@link UserProfileVO} 个人中心用户信息 + */ + UserProfileVO getUserProfile(Long userId); + + /** + * 修改个人中心用户信息 + * + * @param formData 表单数据 + * @return {@link Boolean} 是否修改成功 + */ + boolean updateUserProfile(UserProfileForm formData); + + /** + * 修改用户密码 + * + * @param userId 用户ID + * @param data 修改密码表单数据 + * @return {@link Boolean} 是否修改成功 + */ + boolean changePassword(Long userId, PasswordUpdateForm data); + + /** + * 重置用户密码 + * + * @param userId 用户ID + * @param password 重置后的密码 + * @return {@link Boolean} 是否重置成功 + */ + boolean resetPassword(Long userId, String password); + + /** + * 发送短信验证码(绑定或更换手机号) + * + * @param mobile 手机号 + * @return {@link Boolean} 是否发送成功 + */ + boolean sendMobileCode(String mobile); + + /** + * 修改当前用户手机号 + * + * @param data 表单数据 + * @return {@link Boolean} 是否修改成功 + */ + boolean bindOrChangeMobile(MobileUpdateForm data); + + /** + * 发送邮箱验证码(绑定或更换邮箱) + * + * @param email 邮箱 + */ + void sendEmailCode(String email); + + /** + * 绑定或更换邮箱 + * + * @param data 表单数据 + * @return {@link Boolean} 是否绑定成功 + */ + boolean bindOrChangeEmail(EmailUpdateForm data); + + /** + * 获取用户选项列表 + * + * @return {@link List>} 用户选项列表 + */ + List> listUserOptions(); + + /** + * 根据 openid 获取用户认证信息 + * + * @param username 用户名 + * @return {@link UserAuthInfo} + */ + + UserAuthInfo getUserAuthInfoByOpenId(String username); + + /** + * 根据微信 OpenID 注册或绑定用户 + * + * @param openId 微信 OpenID + */ + void registerOrBindWechatUser(String openId); + + /** + * 根据手机号获取用户认证信息 + * + * @param mobile 手机号 + * @return {@link UserAuthInfo} + */ + UserAuthInfo getUserAuthInfoByMobile(String mobile); + + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/ConfigServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/ConfigServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..27c71b47f2d2ad898c93a42edf66ed314fb30470 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/ConfigServiceImpl.java @@ -0,0 +1,167 @@ +package tech.hypersense.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.system.converter.ConfigConverter; +import tech.hypersense.system.mapper.ConfigMapper; +import tech.hypersense.system.model.entity.Config; +import tech.hypersense.system.model.form.ConfigForm; +import tech.hypersense.system.model.query.ConfigPageQuery; +import tech.hypersense.system.model.vo.ConfigVO; +import tech.hypersense.system.service.ConfigService; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统配置Service接口实现 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class ConfigServiceImpl extends ServiceImpl implements ConfigService { + + + private final ConfigConverter configConverter; + + private final RedisTemplate redisTemplate; + + + /** + * 系统启动完成后,加载系统配置到缓存 + */ + @PostConstruct + public void init() { + refreshCache(); + } + + /** + * 分页查询系统配置 + * + * @param configPageQuery 查询参数 + * @return 系统配置分页列表 + */ + @Override + public IPage page(ConfigPageQuery configPageQuery) { + Page page = new Page<>(configPageQuery.getPageNum(), configPageQuery.getPageSize()); + String keywords = configPageQuery.getKeywords(); + LambdaQueryWrapper query = new LambdaQueryWrapper() + .and(StringUtils.isNotBlank(keywords), + q -> q.like(Config::getConfigKey, keywords) + .or() + .like(Config::getConfigName, keywords) + ); + Page pageList = this.page(page, query); + return configConverter.toPageVo(pageList); + } + + /** + * 保存系统配置 + * + * @param configForm 系统配置表单 + * @return 是否保存成功 + */ + @Override + public boolean save(ConfigForm configForm) { + Assert.isTrue( + super.count(new LambdaQueryWrapper().eq(Config::getConfigKey, configForm.getConfigKey())) == 0, + "配置键已存在"); + Config config = configConverter.toEntity(configForm); + config.setCreateBy(SecurityUtils.getUserId()); + config.setIsDeleted(0); + return this.save(config); + } + + /** + * 获取系统配置表单数据 + * + * @param id 系统配置ID + * @return 系统配置表单数据 + */ + @Override + public ConfigForm getConfigFormData(Long id) { + Config entity = this.getById(id); + return configConverter.toForm(entity); + } + + /** + * 编辑系统配置 + * + * @param id 系统配置ID + * @param configForm 系统配置表单 + * @return 是否编辑成功 + */ + @Override + public boolean edit(Long id, ConfigForm configForm) { + Assert.isTrue( + super.count(new LambdaQueryWrapper().eq(Config::getConfigKey, configForm.getConfigKey()).ne(Config::getId, id)) == 0, + "配置键已存在"); + Config config = configConverter.toEntity(configForm); + config.setUpdateBy(SecurityUtils.getUserId()); + return this.updateById(config); + } + + /** + * 删除系统配置 + * + * @param id 系统配置ID + * @return 是否删除成功 + */ + @Override + public boolean delete(Long id) { + if (id != null) { + return super.update(new LambdaUpdateWrapper() + .eq(Config::getId,id) + .set(Config::getIsDeleted, 1) + .set(Config::getUpdateBy, SecurityUtils.getUserId()) + ); + } + return false; + } + + /** + * 刷新系统配置缓存 + * + * @return 是否刷新成功 + */ + @Override + public boolean refreshCache() { + redisTemplate.delete(RedisConstants.System.CONFIG); + List list = this.list(); + if (list != null) { + Map map = list.stream().collect(Collectors.toMap(Config::getConfigKey, Config::getConfigValue)); + redisTemplate.opsForHash().putAll(RedisConstants.System.CONFIG, map); + return true; + } + return false; + } + + /** + * 获取系统配置 + * + * @param key 配置键 + * @return 配置值 + */ + @Override + public Object getSystemConfig(String key) { + if (StringUtils.isNotBlank(key)) { + return redisTemplate.opsForHash().get(RedisConstants.System.CONFIG, key); + } + return null; + } + +} \ No newline at end of file diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DeptServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DeptServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..819bdffb6611c15ed76a26c57eca8fde564d120e --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DeptServiceImpl.java @@ -0,0 +1,273 @@ +package tech.hypersense.system.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.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.enums.StatusEnum; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.system.converter.DeptConverter; +import tech.hypersense.system.mapper.DeptMapper; +import tech.hypersense.system.model.entity.Dept; +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.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 部门业务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class DeptServiceImpl extends ServiceImpl implements DeptService { + + + private final DeptConverter deptConverter; + + /** + * 获取部门列表 + */ + @Override + public List getDeptList(DeptQuery queryParams) { + // 查询参数 + String keywords = queryParams.getKeywords(); + Integer status = queryParams.getStatus(); + + // 查询数据 + List deptList = this.list( + new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(keywords), Dept::getName, keywords) + .eq(status != null, Dept::getStatus, status) + .orderByAsc(Dept::getSort) + ); + + if (CollectionUtil.isEmpty(deptList)) { + return Collections.EMPTY_LIST; + } + + // 获取所有部门ID + Set deptIds = deptList.stream() + .map(Dept::getId) + .collect(Collectors.toSet()); + // 获取父节点ID + Set parentIds = deptList.stream() + .map(Dept::getParentId) + .collect(Collectors.toSet()); + // 获取根节点ID(递归的起点),即父节点ID中不包含在部门ID中的节点,注意这里不能拿顶级部门 O 作为根节点,因为部门筛选的时候 O 会被过滤掉 + List rootIds = CollectionUtil.subtractToList(parentIds, deptIds); + + // 递归生成部门树形列表 + return rootIds.stream() + .flatMap(rootId -> recurDeptList(rootId, deptList).stream()) + .toList(); + } + + /** + * 递归生成部门树形列表 + * + * @param parentId 父ID + * @param deptList 部门列表 + * @return 部门树形列表 + */ + public List recurDeptList(Long parentId, List deptList) { + return deptList.stream() + .filter(dept -> dept.getParentId().equals(parentId)) + .map(dept -> { + DeptVO deptVO = deptConverter.toVo(dept); + List children = recurDeptList(dept.getId(), deptList); + deptVO.setChildren(children); + return deptVO; + }).toList(); + } + + /** + * 部门下拉选项 + * + * @return 部门下拉List集合 + */ + @Override + public List> listDeptOptions() { + + List deptList = this.list(new LambdaQueryWrapper() + .eq(Dept::getStatus, StatusEnum.ENABLE.getValue()) + .select(Dept::getId, Dept::getParentId, Dept::getName) + .orderByAsc(Dept::getSort) + ); + if (CollectionUtil.isEmpty(deptList)) { + return Collections.EMPTY_LIST; + } + + Set deptIds = deptList.stream() + .map(Dept::getId) + .collect(Collectors.toSet()); + + Set parentIds = deptList.stream() + .map(Dept::getParentId) + .collect(Collectors.toSet()); + + List rootIds = CollectionUtil.subtractToList(parentIds, deptIds); + + // 递归生成部门树形列表 + return rootIds.stream() + .flatMap(rootId -> recurDeptTreeOptions(rootId, deptList).stream()) + .toList(); + } + + /** + * 新增部门 + * + * @param formData 部门表单 + * @return 部门ID + */ + @Override + public Long saveDept(DeptForm formData) { + // 校验部门名称是否存在 + String code = formData.getCode(); + long count = this.count(new LambdaQueryWrapper() + .eq(Dept::getCode, code) + ); + Assert.isTrue(count == 0, "部门编号已存在"); + + // form->entity + Dept entity = deptConverter.toEntity(formData); + + // 生成部门路径(tree_path),格式:父节点tree_path + , + 父节点ID,用于删除部门时级联删除子部门 + String treePath = generateDeptTreePath(formData.getParentId()); + entity.setTreePath(treePath); + + entity.setCreateBy(SecurityUtils.getUserId()); + // 保存部门并返回部门ID + boolean result = this.save(entity); + Assert.isTrue(result, "部门保存失败"); + + return entity.getId(); + } + + + /** + * 获取部门表单 + * + * @param deptId 部门ID + * @return 部门表单对象 + */ + @Override + public DeptForm getDeptForm(Long deptId) { + Dept entity = this.getById(deptId); + return deptConverter.toForm(entity); + } + + + /** + * 更新部门 + * + * @param deptId 部门ID + * @param formData 部门表单 + * @return 部门ID + */ + @Override + public Long updateDept(Long deptId, DeptForm formData) { + // 校验部门名称/部门编号是否存在 + String code = formData.getCode(); + long count = this.count(new LambdaQueryWrapper() + .ne(Dept::getId, deptId) + .eq(Dept::getCode, code) + ); + Assert.isTrue(count == 0, "部门编号已存在"); + + + // form->entity + Dept entity = deptConverter.toEntity(formData); + entity.setId(deptId); + + // 生成部门路径(tree_path),格式:父节点tree_path + , + 父节点ID,用于删除部门时级联删除子部门 + String treePath = generateDeptTreePath(formData.getParentId()); + entity.setTreePath(treePath); + + // 保存部门并返回部门ID + boolean result = this.updateById(entity); + Assert.isTrue(result, "部门更新失败"); + + return entity.getId(); + } + + /** + * 递归生成部门表格层级列表 + * + * @param parentId 父ID + * @param deptList 部门列表 + * @return 部门表格层级列表 + */ + public static List> recurDeptTreeOptions(long parentId, List deptList) { + return CollectionUtil.emptyIfNull(deptList).stream() + .filter(dept -> dept.getParentId().equals(parentId)) + .map(dept -> { + Option option = new Option<>(dept.getId(), dept.getName()); + List> children = recurDeptTreeOptions(dept.getId(), deptList); + if (CollectionUtil.isNotEmpty(children)) { + option.setChildren(children); + } + return option; + }) + .collect(Collectors.toList()); + } + + + /** + * 删除部门 + * + * @param ids 部门ID,多个以英文逗号,拼接字符串 + * @return 是否删除成功 + */ + @Override + public boolean deleteByIds(String ids) { + // 删除部门及子部门 + if (StrUtil.isNotBlank(ids)) { + String[] menuIds = ids.split(","); + for (String deptId : menuIds) { + this.update(new LambdaUpdateWrapper() + .eq(Dept::getId, deptId) + .or() + .apply("CONCAT (',',tree_path,',') LIKE CONCAT('%,',{0},',%')", deptId) + .set(Dept::getIsDeleted, 1) + .set(Dept::getUpdateBy, SecurityUtils.getUserId()) + ); + } + } + return true; + } + + + /** + * 部门路径生成 + * + * @param parentId 父ID + * @return 父节点路径以英文逗号(, )分割,eg: 1,2,3 + */ + private String generateDeptTreePath(Long parentId) { + String treePath = null; + if (SystemConstants.ROOT_NODE_ID.equals(parentId)) { + treePath = String.valueOf(parentId); + } else { + Dept parent = this.getById(parentId); + if (parent != null) { + treePath = parent.getTreePath() + "," + parent.getId(); + } + } + return treePath; + } +} + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DictDataServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DictDataServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..fe3878539c0f5a680e71bf34901da52bb3c6983d --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DictDataServiceImpl.java @@ -0,0 +1,108 @@ +package tech.hypersense.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.system.converter.DictDataConverter; +import tech.hypersense.system.mapper.DictDataMapper; +import tech.hypersense.system.model.entity.DictData; +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.Arrays; +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 字典数据实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class DictDataServiceImpl extends ServiceImpl implements DictDataService { + + private final DictDataConverter dictDataConverter; + + /** + * 获取字典数据分页列表 + * + * @param queryParams + * @return + */ + @Override + public Page getDictDataPage(DictDataPageQuery queryParams) { + int pageNum = queryParams.getPageNum(); + int pageSize = queryParams.getPageSize(); + Page page = new Page<>(pageNum, pageSize); + + return this.baseMapper.getDictDataPage(page, queryParams); + } + + /** + * 获取字典数据表单 + * + * @param id 字典数据ID + * @return + */ + @Override + public DictDataForm getDictDataForm(Long id) { + DictData entity = this.getById(id); + return dictDataConverter.toForm(entity); + } + + /** + * 保存字典数据 + * + * @param formData + * @return + */ + @Override + public boolean saveDictData(DictDataForm formData) { + DictData entity = dictDataConverter.toEntity(formData); + return this.save(entity); + } + + /** + * 更新字典数据 + * + * @param formData 字典数据表单 + * @return + */ + @Override + public boolean updateDictData(DictDataForm formData) { + DictData entity = dictDataConverter.toEntity(formData); + return this.updateById(entity); + } + + /** + * 删除字典数据 + * + * @param ids 字典数据ID集合 + */ + @Override + public void deleteDictDataByIds(String ids) { + List idList = Arrays.stream(ids.split(",")).map(Long::parseLong).toList(); + this.removeByIds(idList); + } + + /** + * 获取字典数据列表 + * + * @param dictCode 字典编码 + * @return + */ + @Override + public List> getDictDataList(String dictCode) { + return this.list(new LambdaQueryWrapper() + .eq(DictData::getDictCode, dictCode) + .eq(DictData::getStatus, 1) + ).stream().map(item -> new Option<>(item.getValue(), item.getLabel(),item.getTagType())) + .toList(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DictServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DictServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3603515bd6e8ea2253d287205b2bf5b4d4d822e5 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/DictServiceImpl.java @@ -0,0 +1,147 @@ +package tech.hypersense.system.service.impl; + +import cn.hutool.core.lang.Assert; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.system.converter.DictConverter; +import tech.hypersense.system.converter.DictDataConverter; +import tech.hypersense.system.mapper.DictMapper; +import tech.hypersense.system.model.entity.Dict; +import tech.hypersense.system.model.entity.DictData; +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.DictDataService; +import tech.hypersense.system.service.DictService; + +import java.util.List; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 数据字典业务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class DictServiceImpl extends ServiceImpl implements DictService { + + private final DictDataService dictDataService; + private final DictConverter dictConverter; + private final DictDataConverter dictDataConverter; + + /** + * 字典分页列表 + * + * @param queryParams 分页查询对象 + */ + @Override + public Page getDictPage(DictPageQuery queryParams) { + // 查询参数 + int pageNum = queryParams.getPageNum(); + int pageSize = queryParams.getPageSize(); + + // 查询数据 + return this.baseMapper.getDictPage(new Page<>(pageNum, pageSize), queryParams); + } + + /** + * 新增字典 + * + * @param dictForm 字典表单数据 + */ + @Override + public boolean saveDict(DictForm dictForm) { + // 保存字典 + Dict entity = dictConverter.toEntity(dictForm); + + // 校验 code 是否唯一 + String dictCode = entity.getDictCode(); + + long count = this.count(new LambdaQueryWrapper() + .eq(Dict::getDictCode, dictCode) + ); + + Assert.isTrue(count == 0, "字典编码已存在"); + + return this.save(entity); + } + + + /** + * 获取字典表单详情 + * + * @param id 字典ID + */ + @Override + public DictForm getDictForm(Long id) { + // 获取字典 + Dict entity = this.getById(id); + if (entity == null) { + throw new BusinessException("字典不存在"); + } + return dictConverter.toForm(entity); + } + + /** + * 修改字典 + * + * @param id 字典ID + * @param dictForm 字典表单 + */ + @Override + public boolean updateDict(Long id, DictForm dictForm) { + // 更新字典 + Dict entity = dictConverter.toEntity(dictForm); + + // 校验 code 是否唯一 + String dictCode = entity.getDictCode(); + long count = this.count(new LambdaQueryWrapper() + .eq(Dict::getDictCode, dictCode) + .ne(Dict::getId, id) + ); + if (count > 0) { + throw new BusinessException("字典编码已存在"); + } + + return this.updateById(entity); + } + + /** + * 删除字典 + * + * @param ids 字典ID,多个以英文逗号(,)分割 + */ + @Override + @Transactional + public void deleteDictByIds(List ids) { + for (String id : ids) { + Dict dict = this.getById(id); + if (dict != null) { + boolean removeResult = this.removeById(id); + // 删除字典下的字典项 + if (removeResult) { + dictDataService.remove( + new LambdaQueryWrapper() + .eq(DictData::getDictCode, dict.getDictCode()) + ); + } + + } + } + } + + /** + * 获取字典列表(包含字典数据) + */ + @Override + public List getAllDictWithData() { + return this.baseMapper.getAllDictWithData(); + } +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/LogServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/LogServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1387ce2932556874b924aec9595943c6116064bb --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/LogServiceImpl.java @@ -0,0 +1,134 @@ +package tech.hypersense.system.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.event.common.log.OperateLogEvent; +import tech.hypersense.common.core.utils.MapstructUtils; +import tech.hypersense.system.mapper.LogMapper; +import tech.hypersense.system.model.bo.LogBo; +import tech.hypersense.system.model.bo.VisitCount; +import tech.hypersense.system.model.bo.VisitStatsBO; +import tech.hypersense.system.model.entity.Log; +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; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 系统日志服务实现类 + * @Version: 1.0 + */ +@Service +public class LogServiceImpl extends ServiceImpl + implements LogService { + + /** + * 操作日志记录 + * + * @param operLogEvent 操作日志事件 + */ + @Async + @EventListener + public void recordOper(OperateLogEvent operLogEvent) { + Log operLog = MapstructUtils.convert(operLogEvent, Log.class); + // 远程查询操作地点 + save(operLog); + } + + + + /** + * 获取日志分页列表 + * + * @param queryParams 查询参数 + * @return 日志分页列表 + */ + @Override + public Page getLogPage(LogPageQuery queryParams) { + return this.baseMapper.getLogPage(new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), + queryParams); + } + + /** + * 获取访问趋势 + * + * @param startDate 开始时间 + * @param endDate 结束时间 + * @return + */ + @Override + public VisitTrendVO getVisitTrend(LocalDate startDate, LocalDate endDate) { + VisitTrendVO visitTrend = new VisitTrendVO(); + List dates = new ArrayList<>(); + + // 获取日期范围内的日期 + while (!startDate.isAfter(endDate)) { + dates.add(startDate.toString()); + startDate = startDate.plusDays(1); + } + visitTrend.setDates(dates); + + // 获取访问量和访问 IP 数的统计数据 + List pvCounts = this.baseMapper.getPvCounts(dates.get(0) + " 00:00:00", dates.get(dates.size() - 1) + " 23:59:59"); + List ipCounts = this.baseMapper.getIpCounts(dates.get(0) + " 00:00:00", dates.get(dates.size() - 1) + " 23:59:59"); + + // 将统计数据转换为 Map + Map pvMap = pvCounts.stream().collect(Collectors.toMap(VisitCount::getDate, VisitCount::getCount)); + Map ipMap = ipCounts.stream().collect(Collectors.toMap(VisitCount::getDate, VisitCount::getCount)); + + // 匹配日期和访问量/访问 IP 数 + List pvList = new ArrayList<>(); + List ipList = new ArrayList<>(); + + for (String date : dates) { + pvList.add(pvMap.getOrDefault(date, 0)); + ipList.add(ipMap.getOrDefault(date, 0)); + } + + visitTrend.setPvList(pvList); + visitTrend.setIpList(ipList); + + return visitTrend; + } + + /** + * 访问量统计 + */ + @Override + public VisitStatsVO getVisitStats() { + VisitStatsVO result = new VisitStatsVO(); + + // 访客数统计(UV) + VisitStatsBO uvStats = this.baseMapper.getUvStats(); + if(uvStats!=null){ + result.setTodayUvCount(uvStats.getTodayCount()); + result.setTotalUvCount(uvStats.getTotalCount()); + result.setUvGrowthRate(uvStats.getGrowthRate()); + } + + // 浏览量统计(PV) + VisitStatsBO pvStats = this.baseMapper.getPvStats(); + if(pvStats!=null){ + result.setTodayPvCount(pvStats.getTodayCount()); + result.setTotalPvCount(pvStats.getTotalCount()); + result.setPvGrowthRate(pvStats.getGrowthRate()); + } + + return result; + } + +} + + diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/MenuServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/MenuServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..769a05772c893ed9b735fd62f149ba3b6f885055 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/MenuServiceImpl.java @@ -0,0 +1,471 @@ +package tech.hypersense.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.core.domain.model.KValue; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.domain.model.shared.codegen.entity.GenConfig; +import tech.hypersense.common.core.enums.StatusEnum; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.system.converter.MenuConverter; +import tech.hypersense.system.enums.MenuTypeEnum; +import tech.hypersense.system.mapper.MenuMapper; +import tech.hypersense.system.model.entity.Menu; +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 tech.hypersense.system.service.RoleMenuService; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 菜单服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class MenuServiceImpl extends ServiceImpl implements MenuService { + + private final MenuConverter menuConverter; + + private final RoleMenuService roleMenuService; + + + /** + * 菜单列表 + * + * @param queryParams {@link MenuQuery} + */ + @Override + public List listMenus(MenuQuery queryParams) { + List menus = this.list(new LambdaQueryWrapper() + .like(StrUtil.isNotBlank(queryParams.getKeywords()), Menu::getName, queryParams.getKeywords()) + .orderByAsc(Menu::getSort) + ); + // 获取所有菜单ID + Set menuIds = menus.stream() + .map(Menu::getId) + .collect(Collectors.toSet()); + + // 获取所有父级ID + Set parentIds = menus.stream() + .map(Menu::getParentId) + .collect(Collectors.toSet()); + + // 获取根节点ID(递归的起点),即父节点ID中不包含在部门ID中的节点,注意这里不能拿顶级菜单 O 作为根节点,因为菜单筛选的时候 O 会被过滤掉 + List rootIds = parentIds.stream() + .filter(id -> !menuIds.contains(id)) + .toList(); + + // 使用递归函数来构建菜单树 + return rootIds.stream() + .flatMap(rootId -> buildMenuTree(rootId, menus).stream()) + .collect(Collectors.toList()); + } + + /** + * 递归生成菜单列表 + * + * @param parentId 父级ID + * @param menuList 菜单列表 + * @return 菜单列表 + */ + private List buildMenuTree(Long parentId, List menuList) { + return CollectionUtil.emptyIfNull(menuList) + .stream() + .filter(menu -> menu.getParentId().equals(parentId)) + .map(entity -> { + MenuVO menuVO = menuConverter.toVo(entity); + List children = buildMenuTree(entity.getId(), menuList); + menuVO.setChildren(children); + return menuVO; + }).toList(); + } + + /** + * 菜单下拉数据 + * + * @param onlyParent 是否只查询父级菜单 如果为true,排除按钮 + */ + @Override + public List> listMenuOptions(boolean onlyParent) { + List menuList = this.list(new LambdaQueryWrapper() + .in(onlyParent, Menu::getType, MenuTypeEnum.CATALOG.getValue(), MenuTypeEnum.MENU.getValue()) + .orderByAsc(Menu::getSort) + ); + return buildMenuOptions(SystemConstants.ROOT_NODE_ID, menuList); + } + + /** + * 递归生成菜单下拉层级列表 + * + * @param parentId 父级ID + * @param menuList 菜单列表 + * @return 菜单下拉列表 + */ + private List> buildMenuOptions(Long parentId, List menuList) { + List> menuOptions = new ArrayList<>(); + + for (Menu menu : menuList) { + if (menu.getParentId().equals(parentId)) { + Option option = new Option<>(menu.getId(), menu.getName()); + List> subMenuOptions = buildMenuOptions(menu.getId(), menuList); + if (!subMenuOptions.isEmpty()) { + option.setChildren(subMenuOptions); + } + menuOptions.add(option); + } + } + + return menuOptions; + } + + /** + * 获取菜单路由列表 + */ + @Override + public List getCurrentUserRoutes() { + + Set roleCodes = SecurityUtils.getRoles(); + + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptyList(); + } + List menuList; + if (SecurityUtils.isRoot()) { + // 超级管理员获取所有菜单 + menuList = this.list(new LambdaQueryWrapper() + .ne(Menu::getType, MenuTypeEnum.BUTTON.getValue()) + .orderByAsc(Menu::getSort) + ); + } else { + menuList = this.baseMapper.getMenusByRoleCodes(roleCodes); + } + return buildRoutes(SystemConstants.ROOT_NODE_ID, menuList); + } + + /** + * 递归生成菜单路由层级列表 + * + * @param parentId 父级ID + * @param menuList 菜单列表 + * @return 路由层级列表 + */ + private List buildRoutes(Long parentId, List menuList) { + List routeList = new ArrayList<>(); + + for (Menu menu : menuList) { + if (menu.getParentId().equals(parentId)) { + RouteVO routeVO = toRouteVo(menu); + List children = buildRoutes(menu.getId(), menuList); + if (!children.isEmpty()) { + routeVO.setChildren(children); + } + routeList.add(routeVO); + } + } + + return routeList; + } + + /** + * 根据RouteBO创建RouteVO + */ + private RouteVO toRouteVo(Menu menu) { + RouteVO routeVO = new RouteVO(); + // 获取路由名称 + String routeName = menu.getRouteName(); + if (StrUtil.isBlank(routeName)) { + // 路由 name 需要驼峰,首字母大写 + routeName = StringUtils.capitalize(StrUtil.toCamelCase(menu.getRoutePath(), '-')); + } + // 根据name路由跳转 this.$router.push({name:xxx}) + routeVO.setName(routeName); + + // 根据path路由跳转 this.$router.push({path:xxx}) + routeVO.setPath(menu.getRoutePath()); + routeVO.setRedirect(menu.getRedirect()); + routeVO.setComponent(menu.getComponent()); + + RouteVO.Meta meta = new RouteVO.Meta(); + meta.setTitle(menu.getName()); + meta.setIcon(menu.getIcon()); + meta.setHidden(StatusEnum.DISABLE.getValue().equals(menu.getVisible())); + // 【菜单】是否开启页面缓存 + if (MenuTypeEnum.MENU.equals(menu.getType()) + && ObjectUtil.equals(menu.getKeepAlive(), 1)) { + meta.setKeepAlive(true); + } + meta.setAlwaysShow(ObjectUtil.equals(menu.getAlwaysShow(), 1)); + + String paramsJson = menu.getParams(); + // 将 JSON 字符串转换为 Map + if (StrUtil.isNotBlank(paramsJson)) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + Map paramMap = objectMapper.readValue(paramsJson, new TypeReference<>() { + }); + meta.setParams(paramMap); + } catch (Exception e) { + throw new RuntimeException("解析参数失败", e); + } + } + routeVO.setMeta(meta); + return routeVO; + } + + /** + * 新增/修改菜单 + */ + @Override + @CacheEvict(cacheNames = "menu", key = "'routes'") + public boolean saveMenu(MenuForm menuForm) { + + MenuTypeEnum menuType = menuForm.getType(); + + if (menuType == MenuTypeEnum.CATALOG) { // 如果是目录 + String path = menuForm.getRoutePath(); + if (menuForm.getParentId() == 0 && !path.startsWith("/")) { + menuForm.setRoutePath("/" + path); // 一级目录需以 / 开头 + } + menuForm.setComponent("Layout"); + } else if (menuType == MenuTypeEnum.EXTLINK) { // 如果是外链 + + menuForm.setComponent(null); + } + if (Objects.equals(menuForm.getParentId(), menuForm.getId())) { + throw new RuntimeException("父级菜单不能为当前菜单"); + } + Menu entity = menuConverter.toEntity(menuForm); + String treePath = generateMenuTreePath(menuForm.getParentId()); + entity.setTreePath(treePath); + + List params = menuForm.getParams(); + // 路由参数 [{key:"id",value:"1"},{key:"name",value:"张三"}] 转换为 [{"id":"1"},{"name":"张三"}] + if (CollectionUtil.isNotEmpty(params)) { + entity.setParams(JSONUtil.toJsonStr(params.stream() + .collect(Collectors.toMap(KValue::getKey, KValue::getValue)))); + } else { + entity.setParams(null); + } + // 新增类型为菜单时候 路由名称唯一 + if (MenuTypeEnum.MENU.equals(menuType)) { + Assert.isFalse(this.exists(new LambdaQueryWrapper() + .eq(Menu::getRouteName, entity.getRouteName()) + .ne(menuForm.getId() != null, Menu::getId, menuForm.getId()) + ), "路由名称已存在"); + } else { + // 其他类型时 给路由名称赋值为空 + entity.setRouteName(null); + } + + boolean result = this.saveOrUpdate(entity); + if (result) { + // 编辑刷新角色权限缓存 + if (menuForm.getId() != null) { + roleMenuService.refreshRolePermsCache(); + } + } + // 修改菜单如果有子菜单,则更新子菜单的树路径 + updateChildrenTreePath(entity.getId(), treePath); + return result; + } + + /** + * 更新子菜单树路径 + * + * @param id 当前菜单ID + * @param treePath 当前菜单树路径 + */ + private void updateChildrenTreePath(Long id, String treePath) { + List children = this.list(new LambdaQueryWrapper().eq(Menu::getParentId, id)); + if (CollectionUtil.isNotEmpty(children)) { + // 子菜单的树路径等于父菜单的树路径加上父菜单ID + String childTreePath = treePath + "," + id; + this.update(new LambdaUpdateWrapper() + .eq(Menu::getParentId, id) + .set(Menu::getTreePath, childTreePath) + ); + for (Menu child : children) { + // 递归更新子菜单 + updateChildrenTreePath(child.getId(), childTreePath); + } + } + } + + /** + * 部门路径生成 + * + * @param parentId 父ID + * @return 父节点路径以英文逗号(, )分割,eg: 1,2,3 + */ + private String generateMenuTreePath(Long parentId) { + if (SystemConstants.ROOT_NODE_ID.equals(parentId)) { + return String.valueOf(parentId); + } else { + Menu parent = this.getById(parentId); + return parent != null ? parent.getTreePath() + "," + parent.getId() : null; + } + } + + + /** + * 修改菜单显示状态 + * + * @param menuId 菜单ID + * @param visible 是否显示(1->显示;2->隐藏) + * @return 是否修改成功 + */ + @Override + @CacheEvict(cacheNames = "menu", key = "'routes'") + public boolean updateMenuVisible(Long menuId, Integer visible) { + return this.update(new LambdaUpdateWrapper() + .eq(Menu::getId, menuId) + .set(Menu::getVisible, visible) + ); + } + + /** + * 获取菜单表单数据 + * + * @param id 菜单ID + * @return 菜单表单数据 + */ + @Override + public MenuForm getMenuForm(Long id) { + Menu entity = this.getById(id); + Assert.isTrue(entity != null, "菜单不存在"); + MenuForm formData = menuConverter.toForm(entity); + // 路由参数字符串 {"id":"1","name":"张三"} 转换为 [{key:"id", value:"1"}, {key:"name", value:"张三"}] + String params = entity.getParams(); + if (StrUtil.isNotBlank(params)) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + // 解析 JSON 字符串为 Map + Map paramMap = objectMapper.readValue(params, new TypeReference<>() { + }); + + // 转换为 List 格式 [{key:"id", value:"1"}, {key:"name", value:"张三"}] + List transformedList = paramMap.entrySet().stream() + .map(entry -> new KValue(entry.getKey(), entry.getValue())) + .toList(); + + // 将转换后的列表存入 MenuForm + formData.setParams(transformedList); + } catch (Exception e) { + throw new RuntimeException("解析参数失败", e); + } + } + + return formData; + } + + /** + * 删除菜单 + * + * @param id 菜单ID + * @return 是否删除成功 + */ + @Override + @CacheEvict(cacheNames = "menu", key = "'routes'") + public boolean deleteMenu(Long id) { + boolean result = this.remove(new LambdaQueryWrapper() + .eq(Menu::getId, id) + .or() + .apply("CONCAT (',',tree_path,',') LIKE CONCAT('%,',{0},',%')", id)); + + + // 刷新角色权限缓存 + if (result) { + roleMenuService.refreshRolePermsCache(); + } + return result; + + } + + /** + * 代码生成时添加菜单 + * + * @param parentMenuId 父菜单ID + * @param genConfig 实体名称 + */ + @Override + public void addMenuForCodegen(Long parentMenuId, GenConfig genConfig) { + Menu parentMenu = this.getById(parentMenuId); + Assert.notNull(parentMenu, "上级菜单不存在"); + + String entityName = genConfig.getEntityName(); + + long count = this.count(new LambdaQueryWrapper().eq(Menu::getRouteName, entityName)); + if (count > 0) { + return; + } + + // 获取父级菜单子菜单最带的排序 + Menu maxSortMenu = this.getOne(new LambdaQueryWrapper().eq(Menu::getParentId, parentMenuId) + .orderByDesc(Menu::getSort) + .last("limit 1") + ); + int sort = 1; + if (maxSortMenu != null) { + sort = maxSortMenu.getSort() + 1; + } + + Menu menu = new Menu(); + menu.setParentId(parentMenuId); + menu.setName(genConfig.getBusinessName()); + + menu.setRouteName(entityName); + menu.setRoutePath(StrUtil.toSymbolCase(entityName, '-')); + menu.setComponent(genConfig.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index"); + menu.setType(MenuTypeEnum.MENU); + menu.setSort(sort); + menu.setVisible(1); + boolean result = this.save(menu); + + if (result) { + // 生成treePath + String treePath = generateMenuTreePath(parentMenuId); + menu.setTreePath(treePath); + this.updateById(menu); + + // 生成CURD按钮权限 + String permPrefix = genConfig.getModuleName() + ":" + StrUtil.lowerFirst(entityName) + ":"; + String[] actions = {"查询", "新增", "编辑", "删除"}; + String[] perms = {"query", "add", "edit", "delete"}; + + for (int i = 0; i < actions.length; i++) { + Menu button = new Menu(); + button.setParentId(menu.getId()); + button.setType(MenuTypeEnum.BUTTON); + button.setName(actions[i]); + button.setPerm(permPrefix + perms[i]); + button.setSort(i + 1); + this.save(button); + + // 生成 treepath + button.setTreePath(treePath + "," + button.getId()); + this.updateById(button); + } + } + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/NoticeServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/NoticeServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..cafb09e141d0fe50a765d5a4f2aa32be6302aa76 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/NoticeServiceImpl.java @@ -0,0 +1,299 @@ +package tech.hypersense.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.common.websocket.service.OnlineUserService; +import tech.hypersense.system.converter.NoticeConverter; +import tech.hypersense.system.enums.NoticePublishStatusEnum; +import tech.hypersense.system.enums.NoticeTargetEnum; +import tech.hypersense.system.mapper.NoticeMapper; +import tech.hypersense.system.model.bo.NoticeBO; +import tech.hypersense.system.model.dto.NoticeDTO; +import tech.hypersense.system.model.entity.Notice; +import tech.hypersense.system.model.entity.User; +import tech.hypersense.system.model.entity.UserNotice; +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; +import tech.hypersense.system.service.UserService; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 通知公告服务实现类 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class NoticeServiceImpl extends ServiceImpl implements NoticeService { + + private final NoticeConverter noticeConverter; + private final UserNoticeService userNoticeService; + private final UserService userService; + private final SimpMessagingTemplate messagingTemplate; + private final OnlineUserService onlineUserService; + + /** + * 获取通知公告分页列表 + * + * @param queryParams 查询参数 + * @return {@link IPage < NoticePageVO >} 通知公告分页列表 + */ + @Override + public IPage getNoticePage(NoticePageQuery queryParams) { + Page noticePage = this.baseMapper.getNoticePage( + new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), + queryParams + ); + return noticeConverter.toPageVo(noticePage); + } + + /** + * 获取通知公告表单数据 + * + * @param id 通知公告ID + * @return {@link NoticeForm} 通知公告表单对象 + */ + @Override + public NoticeForm getNoticeFormData(Long id) { + Notice entity = this.getById(id); + return noticeConverter.toForm(entity); + } + + /** + * 新增通知公告 + * + * @param formData 通知公告表单对象 + * @return {@link Boolean} 是否新增成功 + */ + @Override + public boolean saveNotice(NoticeForm formData) { + + if (NoticeTargetEnum.SPECIFIED.getValue().equals(formData.getTargetType())) { + List targetUserIdList = formData.getTargetUserIds(); + if (CollectionUtil.isEmpty(targetUserIdList)) { + throw new BusinessException("推送指定用户不能为空"); + } + } + Notice entity = noticeConverter.toEntity(formData); + entity.setCreateBy(SecurityUtils.getUserId()); + return this.save(entity); + } + + /** + * 更新通知公告 + * + * @param id 通知公告ID + * @param formData 通知公告表单对象 + * @return {@link Boolean} 是否更新成功 + */ + @Override + public boolean updateNotice(Long id, NoticeForm formData) { + if (NoticeTargetEnum.SPECIFIED.getValue().equals(formData.getTargetType())) { + List targetUserIdList = formData.getTargetUserIds(); + if (CollectionUtil.isEmpty(targetUserIdList)) { + throw new BusinessException("推送指定用户不能为空"); + } + } + + Notice entity = noticeConverter.toEntity(formData); + return this.updateById(entity); + } + + /** + * 删除通知公告 + * + * @param ids 通知公告ID,多个以英文逗号(,)分割 + * @return {@link Boolean} 是否删除成功 + */ + @Override + @Transactional + public boolean deleteNotices(String ids) { + if (StrUtil.isBlank(ids)) { + throw new BusinessException("删除的通知公告数据为空"); + } + + // 逻辑删除 + List idList = Arrays.stream(ids.split(",")) + .map(Long::parseLong) + .toList(); + boolean isRemoved = this.removeByIds(idList); + if (isRemoved) { + // 删除通知公告的同时,需要删除通知公告对应的用户通知状态 + userNoticeService.remove(new LambdaQueryWrapper().in(UserNotice::getNoticeId, idList)); + } + return isRemoved; + } + + /** + * 发布通知公告 + * + * @param id 通知公告ID + * @return 是否发布成功 + */ + @Override + @Transactional + public boolean publishNotice(Long id) { + Notice notice = this.getById(id); + if (notice == null) { + throw new BusinessException("通知公告不存在"); + } + + if (NoticePublishStatusEnum.PUBLISHED.getValue().equals(notice.getPublishStatus())) { + throw new BusinessException("通知公告已发布"); + } + + Integer targetType = notice.getTargetType(); + String targetUserIds = notice.getTargetUserIds(); + if (NoticeTargetEnum.SPECIFIED.getValue().equals(targetType) + && StrUtil.isBlank(targetUserIds)) { + throw new BusinessException("推送指定用户不能为空"); + } + + notice.setPublishStatus(NoticePublishStatusEnum.PUBLISHED.getValue()); + notice.setPublisherId(SecurityUtils.getUserId()); + notice.setPublishTime(LocalDateTime.now()); + boolean publishResult = this.updateById(notice); + + if (publishResult) { + // 发布通知公告的同时,删除该通告之前的用户通知数据,因为可能是重新发布 + userNoticeService.remove( + new LambdaQueryWrapper().eq(UserNotice::getNoticeId, id) + ); + + // 添加新的用户通知数据 + List targetUserIdList = null; + if (NoticeTargetEnum.SPECIFIED.getValue().equals(targetType)) { + targetUserIdList = Arrays.asList(targetUserIds.split(",")); + } + + List targetUserList = userService.list( + new LambdaQueryWrapper() + // 如果是指定用户,则筛选出指定用户 + .in( + NoticeTargetEnum.SPECIFIED.getValue().equals(targetType), + User::getId, + targetUserIdList + ) + ); + + List userNoticeList = targetUserList.stream().map(user -> { + UserNotice userNotice = new UserNotice(); + userNotice.setNoticeId(id); + userNotice.setUserId(user.getId()); + userNotice.setIsRead(0); + return userNotice; + }).toList(); + + if (CollectionUtil.isNotEmpty(userNoticeList)) { + userNoticeService.saveBatch(userNoticeList); + } + + Set receivers = targetUserList.stream().map(User::getUsername).collect(Collectors.toSet()); + + Set allOnlineUsers = onlineUserService.getAllOnlineUsers(); + + // 找出在线用户的通知接收者 + Set onlineReceivers = new HashSet<>(CollectionUtil.intersection(receivers, allOnlineUsers)); + + NoticeDTO noticeDTO = new NoticeDTO(); + noticeDTO.setId(id); + noticeDTO.setTitle(notice.getTitle()); + noticeDTO.setType(notice.getType()); + noticeDTO.setPublishTime(notice.getPublishTime()); + + onlineReceivers.forEach(receiver -> messagingTemplate.convertAndSendToUser(receiver, "/queue/message", noticeDTO)); + } + return publishResult; + } + + /** + * 撤回通知公告 + * + * @param id 通知公告ID + * @return 是否撤回成功 + */ + @Override + @Transactional + public boolean revokeNotice(Long id) { + Notice notice = this.getById(id); + if (notice == null) { + throw new BusinessException("通知公告不存在"); + } + + if (!NoticePublishStatusEnum.PUBLISHED.getValue().equals(notice.getPublishStatus())) { + throw new BusinessException("通知公告未发布或已撤回"); + } + + notice.setPublishStatus(NoticePublishStatusEnum.REVOKED.getValue()); + notice.setRevokeTime(LocalDateTime.now()); + notice.setUpdateBy(SecurityUtils.getUserId()); + + boolean revokeResult = this.updateById(notice); + + if (revokeResult) { + // 撤回通知公告的同时,需要删除通知公告对应的用户通知状态 + userNoticeService.remove(new LambdaQueryWrapper() + .eq(UserNotice::getNoticeId, id) + ); + } + return revokeResult; + } + + /** + * + * @param id 通知公告ID + * @return NoticeDetailVO 通知公告详情 + */ + @Override + public NoticeDetailVO getNoticeDetail(Long id) { + NoticeBO noticeBO = this.baseMapper.getNoticeDetail(id); + // 更新用户通知公告的阅读状态 + Long userId = SecurityUtils.getUserId(); + userNoticeService.update(new LambdaUpdateWrapper() + .eq(UserNotice::getNoticeId, id) + .eq(UserNotice::getUserId, userId) + .eq(UserNotice::getIsRead, 0) + .set(UserNotice::getIsRead, 1) + ); + return noticeConverter.toDetailVO(noticeBO); + } + + /** + * 获取当前登录用户的通知公告列表 + * + * @param queryParams 查询参数 + * @return 通知公告分页列表 + */ + @Override + public IPage getMyNoticePage(NoticePageQuery queryParams) { + queryParams.setUserId(SecurityUtils.getUserId()); + return userNoticeService.getMyNoticePage( + new Page<>(queryParams.getPageNum(), queryParams.getPageSize()), + queryParams + ); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/RoleMenuServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/RoleMenuServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..63ea8dc564c34d95bef490d024e3076f8027aa99 --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/RoleMenuServiceImpl.java @@ -0,0 +1,126 @@ +package tech.hypersense.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import tech.hypersense.common.core.constant.RedisConstants; +import tech.hypersense.system.mapper.RoleMenuMapper; +import tech.hypersense.system.model.bo.RolePermsBO; +import tech.hypersense.system.model.entity.RoleMenu; +import tech.hypersense.system.service.RoleMenuService; + +import java.util.List; +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: 角色菜单业务实现 + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class RoleMenuServiceImpl extends ServiceImpl implements RoleMenuService { + + private final RedisTemplate redisTemplate; + + /** + * 初始化权限缓存 + */ + @PostConstruct + public void initRolePermsCache() { + log.info("初始化权限缓存... "); + refreshRolePermsCache(); + } + + /** + * 刷新权限缓存 + */ + @Override + public void refreshRolePermsCache() { + // 清理权限缓存 + redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, "*"); + + List list = this.baseMapper.getRolePermsList(null); + if (CollectionUtil.isNotEmpty(list)) { + list.forEach(item -> { + String roleCode = item.getRoleCode(); + Set perms = item.getPerms(); + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms); + } + }); + } + } + + /** + * 刷新权限缓存 + */ + @Override + public void refreshRolePermsCache(String roleCode) { + // 清理权限缓存 + redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, roleCode); + + List list = this.baseMapper.getRolePermsList(roleCode); + if (CollectionUtil.isNotEmpty(list)) { + RolePermsBO rolePerms = list.get(0); + if (rolePerms == null) { + return; + } + + Set perms = rolePerms.getPerms(); + if (CollectionUtil.isNotEmpty(perms)) { + redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, roleCode, perms); + } + } + } + + /** + * 刷新权限缓存 (角色编码变更时调用) + */ + @Override + public void refreshRolePermsCache(String oldRoleCode, String newRoleCode) { + // 清理旧角色权限缓存 + redisTemplate.opsForHash().delete(RedisConstants.System.ROLE_PERMS, oldRoleCode); + + // 添加新角色权限缓存 + List list = this.baseMapper.getRolePermsList(newRoleCode); + if (CollectionUtil.isNotEmpty(list)) { + RolePermsBO rolePerms = list.get(0); + if (rolePerms == null) { + return; + } + + Set perms = rolePerms.getPerms(); + redisTemplate.opsForHash().put(RedisConstants.System.ROLE_PERMS, newRoleCode, perms); + } + } + + /** + * 获取角色权限集合 + * + * @param roles 角色编码集合 + * @return 权限集合 + */ + @Override + public Set getRolePermsByRoleCodes(Set roles) { + return this.baseMapper.listRolePerms(roles); + } + + /** + * 获取角色拥有的菜单ID集合 + * + * @param roleId 角色ID + * @return 菜单ID集合 + */ + @Override + public List listMenuIdsByRoleId(Long roleId) { + return this.baseMapper.listMenuIdsByRoleId(roleId); + } + +} diff --git a/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/RoleServiceImpl.java b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..e0ea60091af0e195398ba7d734ed2c90211d0d8a --- /dev/null +++ b/hypersense-modules/hypersense-system/src/main/java/tech/hypersense/system/service/impl/RoleServiceImpl.java @@ -0,0 +1,257 @@ +package tech.hypersense.system.service.impl; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tech.hypersense.common.core.constant.SystemConstants; +import tech.hypersense.common.core.domain.model.Option; +import tech.hypersense.common.core.exception.BusinessException; +import tech.hypersense.common.security.utils.SecurityUtils; +import tech.hypersense.system.converter.RoleConverter; +import tech.hypersense.system.mapper.RoleMapper; +import tech.hypersense.system.model.entity.Role; +import tech.hypersense.system.model.entity.RoleMenu; +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.RoleMenuService; +import tech.hypersense.system.service.RoleService; +import tech.hypersense.system.service.UserRoleService; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * @Author: HyperSense + * @CreateTime: 2025-03-27 + * @Description: + * @Version: 1.0 + */ +@Service +@RequiredArgsConstructor +public class RoleServiceImpl extends ServiceImpl implements RoleService { + + private final RoleMenuService roleMenuService; + private final UserRoleService userRoleService; + private final RoleConverter roleConverter; + + /** + * 角色分页列表 + * + * @param queryParams 角色查询参数 + * @return {@link Page < RolePageVO >} – 角色分页列表 + */ + @Override + public Page getRolePage(RolePageQuery queryParams) { + // 查询参数 + int pageNum = queryParams.getPageNum(); + int pageSize = queryParams.getPageSize(); + String keywords = queryParams.getKeywords(); + + // 查询数据 + Page rolePage = this.page(new Page<>(pageNum, pageSize), + new LambdaQueryWrapper() + .and(StrUtil.isNotBlank(keywords), + wrapper -> + wrapper.like(Role::getName, keywords) + .or() + .like(Role::getCode, keywords) + ) + .ne(!SecurityUtils.isRoot(), Role::getCode, SystemConstants.ROOT_ROLE_CODE) // 非超级管理员不显示超级管理员角色 + .orderByAsc(Role::getSort).orderByDesc(Role::getCreateTime).orderByDesc(Role::getUpdateTime) + ); + + // 实体转换 + return roleConverter.toPageVo(rolePage); + } + + /** + * 角色下拉列表 + * + * @return {@link List