84 Commits
Author SHA1 Message Date
OpenCode 8ec1ef7605 feat: SKU新增showStock字段 - 控制是否显示库存
- ProductGoodsSkuEntity: 新增showStock(0:否 1:是)
- 价格/库存均为可空: null=线下议价/无限量
2026-07-28 14:28:20 +08:00
OpenCode 4eb0a51569 feat: 属性新增fillOnPurchase字段 - 标识是否购买时填写
- ProductAttributeEntity: 新增fillOnPurchase(0:否 1:是)字段
- 区分三种属性角色: 规格属性(选择类型)/商家属性(非选择+非购买时填写)/购买时属性(fillOnPurchase=1)
2026-07-28 14:07:21 +08:00
OpenCode fcc52c317b feat: 新增SKU表 - ProductGoodsSkuEntity
- ProductGoodsSkuEntity: goodsId/skuCode/attrs(JSON)/price/stock/image/status
- ProductGoodsSkuMapper/Service/ServiceImpl: CRUD
- AdminProductGoodsSkuController: Admin端SKU接口(page/add/update/delete/info)
2026-07-28 13:48:35 +08:00
OpenCode 3c339f7f43 feat: 行为管理改为自动注册模式 - handler自带name/description
- ActionHandler: 新增getName()/getDescription()方法
- StockCheckHandler/DatePickHandler/DeliveryRouteHandler: 补全name和description
- AdminProductActionController: 改为list(自动从handler生成)+toggle(启用禁用)
- product_action表仅存被禁用的行为记录,启用状态为默认
- RestInterceptor: 声明api={list,toggle}放行
2026-07-28 13:10:11 +08:00
OpenCode 8e87c94053 feat: 行为管理改为注册模式 - handlers接口返回已注册handler列表
- AdminProductActionController: 新增 GET /handlers 返回后端已注册的ActionHandler列表
- ActionHandlerRegistry: 新增 getHandlers() 方法暴露所有handler
- 行为管理只能注册后端已有handler的行为,不可随意创建
2026-07-28 12:57:23 +08:00
OpenCode 85c9929b27 feat: 商品行为管理 + 属性编码 + ActionHandler机制
- ProductActionEntity: 行为管理实体(name/code/description/status)
- ProductAttributeEntity: 新增code(属性编码)和action(关联行为)字段
- ActionHandler接口: validate校验 + afterPurchase购买后处理
- StockCheckHandler: 库存校验+扣减
- DatePickHandler: 服务日期校验
- DeliveryRouteHandler: 发货方式校验
- ActionHandlerRegistry: 自动注册所有handler
2026-07-28 11:55:41 +08:00
OpenCode 24e18d9327 feat: 商品模块重构 - 区分实物/虚拟商品,新增属性管理
- ProductGoodsEntity: 删除price/unit/categoryId,新增type(商品类型)和extra(扩展属性JSON)
- ProductAttributeEntity: 新增属性管理实体(name/type/options/status)
- ProductAttributeMapper/Service/ServiceImpl: 属性CRUD
- AdminProductAttributeController: Admin端属性管理接口
- AppProductAttributeController: App端属性列表接口
2026-07-28 11:36:28 +08:00
OpenCode 7ab1c7590e fix: 修复时间差8小时问题,JacksonConfig 设置 Asia/Shanghai 时区,Dockerfile 添加 TZ 环境变量 2026-07-28 10:04:06 +08:00
OpenCode f51ebc99d1 feat: App端代理列表只显示启用代理+新增checkIpInWhitelist接口+停用联动
- 代理列表只返回status=1的代理,详情接口校验停用状态
- 新增 /app/agent/info/checkIpInWhitelist 接口,查询IP是否在SaySS白名单
- SayssService 新增 isIpInTrustedIps 方法
2026-07-27 22:46:24 +08:00
OpenCode 937d95ae74 feat: SaySS代理模块后端集成 - 登录token存库/白名单增删同步SaySS/添加者用户ID
- AgentInfoEntity 新增 sayssAuthData 字段存储 SaySS auth_data token
- AgentWhitelistEntity 新增 sayssTrustedIpId、addedByUserId 字段,addedByName 虚拟字段
- SayssService 实现 AES-CBC 加密中间件调用,支持 loginAndSave/checkTokenValid/addTrustedIp/deleteTrustedIp
- AdminAgentInfoController 新增 loginSayss、checkToken 接口
- AdminAgentWhitelistController 删除时先从 SaySS 删除成功后再删本地,分页关联查出 agentName 和 addedByName
- AppAgentInfoController addWhitelist 先调 SaySS 添加成功后保存本地,记录当前 App 用户 ID
- application.yml 新增 sayss 配置
2026-07-27 20:40:56 +08:00
OpenCode bf7c79c87f feat: 新增代理白名单模块 - AgentWhitelist实体/Controller/Service/Admin分页 2026-07-27 19:18:51 +08:00
OpenCode 28bbb285c2 fix: 代理App端白名单接口完善 - 添加@CoolRestController(api=list)修复路由注册,queryWrapper非空修复,新增addWhitelist/whitelist/currentIp接口 2026-07-27 19:18:16 +08:00
OpenCode 3dde0f2aa0 feat: 代理模块字段调整 - 删除phone/region/image,增加username/password/tokenStatus 2026-07-27 18:22:52 +08:00
OpenCode a45704af72 feat: 模块注册自动创建Admin菜单 + Admin组织切换生成新JWT
- AppModuleServiceImpl: 模块注册时自动扫描Controller创建菜单树,回写menuIds
- AppOrgServiceImpl: Admin切换组织生成新JWT(含新tenantId);新增listForSwitch方法
- AdminAppOrgController: switchOrg返回token;新增/admin/org/listForSwitch端点
- AppOrgService: 接口增加listForSwitch声明
- AppOrgAuthFilter: proxy改为agent(moduleKey与包名一致)
2026-07-27 15:25:28 +08:00
OpenCode d5992198d2 chore: 移除zoo动物管理模块(后端) 2026-07-27 11:16:05 +08:00
OpenCode 861d7cba46 feat: 组织模块授权支持排序
- AppOrgModuleEntity新增sortOrder字段
- authorizeModules按moduleKeys数组索引设置sortOrder
- getModuleKeysByOrgId按sortOrder升序查询
2026-07-26 23:07:36 +08:00
OpenCode e7ed654abd feat: 新增product商品模块 + agent代理模块(moduleKey:proxy)
- 后端:product模块CRUD(Entity/Mapper/Service/Controller)
- 后端:agent模块CRUD(Entity/Mapper/Service/Controller)
- AppOrgAuthFilter注册/app/product/和/app/agent/路径
- App:新增module-product和module-proxy页面
- pages.json/tabbar/router注册两个新模块
- tabbar移除Star tab,模块上限改为3个
- 修复product页面双标题栏(navigationStyle:custom)
2026-07-26 22:54:21 +08:00
OpenCode c49925636e feat: 组织列表增加成员数列,成员管理支持按组织名筛选和昵称跳转用户列表
- AppOrgEntity 添加 memberCount 虚拟字段(@Column(ignore))
- AdminAppOrgController page查询通过transformValue实时计算成员数
- AppOrgUserService/Impl 新增 getMemberList(orgId, orgName, page, size) 支持按组织名模糊筛选
- AdminAppOrgUserController memberList 接口新增 orgName 参数
2026-07-24 19:22:39 +08:00
OpenCode 5e8bfebfdc feat: 组织模块授权、成员管理分页、角色创建修复
- AppOrgEntity: 添加 @JsonIgnoreProperties(ignoreUnknown=true) 支持moduleKeys参数
- AppOrgServiceImpl: modifyAfter钩子处理UPDATE时自动授权模块
- AppOrgServiceImpl: createOrg中设置role.userId修复NOT NULL约束
- AppOrgServiceImpl: updatePerms第三个参数null改为new Long[0]避免NPE
- AppOrgUserServiceImpl: getMemberList支持分页+可选orgId筛选+组织名称关联
- AdminAppOrgUserController: memberList接口支持page/size分页参数
- AppOrgUserController: App端同步更新getMemberList调用
2026-07-24 14:24:12 +08:00
OpenCode b42b5750ac fix: 修复组织Controller路由路径(cname→value)和菜单viewPath 2026-07-24 10:49:32 +08:00
OpenCode a650a852c0 feat: 添加组织管理菜单初始化数据(menu_org.json) 2026-07-24 09:58:28 +08:00
OpenCode 87b743ae11 feat: 多组织功能 - 实体/Service/Controller/拦截器/租户隔离 2026-07-24 09:31:21 +08:00
OpenCode 80dc8a0c07 feat: 实现邮箱验证码功能(注册、修改密码、修改邮箱)
- UserInfoService 新增 sendEmailCodeForRegister、sendEmailCodeForUpdateEmail、checkEmailCode 方法
- 三种场景使用不同缓存前缀区分:
  - 注册:email_code_register:{email}
  - 修改密码:email_code:{userId}
  - 修改邮箱:email_code_update_email:{userId}
- AppUserLoginController 新增 /sendEmailCodeForRegister 公开接口
- AppUserInfoController 新增 /sendEmailCodeForUpdateEmail 接口
- /bindEmail、/updateEmail、/updatePassword 增加验证码验证
2026-07-23 13:41:00 +08:00
OpenCode 1ecb285fd0 feat: 邮箱配置管理+邮箱验证码修改密码
- 新增 BaseSysEmailConfigEntity 邮箱配置实体及CRUD
- 新增 EmailSendService 邮件发送服务(动态SMTP)
- 新增 sendEmailCode/updatePasswordByEmail 接口
- App修改密码改用邮箱验证码(原短信验证码保留)
- 邮箱配置支持多条,仅一条可设为当前使用
- 新增测试邮件发送接口
- menu.json 新增邮箱配置菜单
- pom.xml 新增 spring-boot-starter-mail 依赖
2026-07-23 13:03:14 +08:00
OpenCode ef9c06edf6 fix: 在requestParams上清除空字符串,修复phone唯一索引冲突 2026-07-22 22:54:57 +08:00
OpenCode 5024d53095 fix: 空字符串转null避免唯一索引冲突 2026-07-22 22:49:25 +08:00
OpenCode 9e6aede480 fix: 用户名为空时后端也禁止修改邮箱 2026-07-22 22:45:07 +08:00
OpenCode 4ce7801737 fix: 去除邮箱修改限制,邮箱始终可修改并校验唯一性 2026-07-22 22:42:49 +08:00
OpenCode 2a9453af15 feat: 编辑用户时用户名/邮箱按规则可修改,修改时校验唯一性 2026-07-22 22:31:45 +08:00
OpenCode f42c2433d0 feat: 管理端重置用户密码,生成8位随机字符串并返回 2026-07-22 22:22:41 +08:00
OpenCode f9fd0a42dd refactor: 注销时用户名加时间戳后缀、邮箱加时间戳前缀 2026-07-22 21:50:43 +08:00
OpenCode 7fc636a020 feat: 新增验证码配置管理功能
- 新增 UserCaptchaConfigEntity 实体类,含 side(admin/app) 端标识
- 新增 Admin/App Controller、Service、Mapper
- BaseSysLoginServiceImpl 新增 isCaptchaEnabled() 查询数据库配置
- login/register 改用数据库配置替代 cool.captcha.enable 硬编码
- AdminBaseOpenController 新增 /captchaEnabled 接口
- menu.json 新增验证码管理菜单及权限
- base.json 新增 5 条预置数据(3条app+2条admin)
2026-07-22 20:11:34 +08:00
OpenCode f5205ef0e9 feat: 用户管理增强 - username字段、注销释放唯一索引、禁用清除缓存 2026-07-22 18:22:16 +08:00
OpenCode 453514017c feat: 登录注册配置CRUD及菜单数据 2026-07-22 15:42:51 +08:00
OpenCode 5de3b3e08c feat: add email and username login for app user 2026-07-21 19:51:39 +08:00
OpenCode a5aece156c feat: 新增解锁接口 2026-07-21 13:17:25 +08:00
OpenCode 0b6ee96645 fix: 修正生产环境配置
- Dockerfile: JRE 17 → 21 匹配实际构建环境
- application-prod.yml: 修正 cool 缩进错误,修复 file.base-url 为相对路径
2026-07-20 19:49:28 +08:00
OpenCode 98d262a68c fix: 循环依赖修复 - BaseSysUserServiceImpl PasswordEncoder加@Lazy 2026-07-20 09:49:26 +08:00
OpenCode 13a90c2573 fix: RBAC模型P3安全修复
P3-13: 密码改用BCrypt+MD5兼容模式
P3-14: 角色继承-增加parentId递归合并父角色权限
P3-15: 权限变更审计日志-新增base_sys_perms_audit_log表
2026-07-18 18:24:36 +08:00
OpenCode 37df40f5e4 fix: RBAC模型P2安全修复
P2-8: 删除实体时级联清理中间表
P2-9: BaseEntity添加delFlag逻辑删除字段
P2-10: adminLogout清理roleIds和perms缓存
P2-11: 启用EnableMethodSecurity,BaseController增加checkPermission钩子
P2-12: 移除eps/getModuleTree/createCode的TokenIgnore
2026-07-17 21:06:32 +08:00
OpenCode e02c7a02dc fix: RBAC模型P0+P1安全修复
P0-1: 中间表更新添加@Transactional事务保护
P0-2: 角色分配增加越权提权校验
P0-3: 未配置权限接口改为默认拒绝策略
P1-4: 实现relevance字段递归扩展子部门
P1-5: move/info接口增加部门权限校验
P1-6: 权限映射缓存变更时自动刷新
P1-7: 租户ID仅从JWT获取,角色/部门实体加租户隔离
2026-07-17 18:59:05 +08:00
OpenCode 17e74bb247 feat: 交易记录锁定功能 - 新增locked字段、lock接口、锁定后禁止修改 2026-07-17 17:20:42 +08:00
OpenCode 0591f2ae46 fix: 修改密码功能-验证旧密码,使用newPassword字段设置新密码 2026-07-16 20:31:21 +08:00
OpenCode 1d880951b1 refactor: 超管判断从硬编码admin用户名改为基于角色判断,支持多超管账号 2026-07-16 20:22:28 +08:00
OpenCode 791f006ce1 fix: 注册后不再自动登录,角色改为普通用户,部门改为待分配部门;修复编辑用户时密码被重复哈希的bug 2026-07-16 19:17:25 +08:00
OpenCode 4bd3c7ab40 feat: 大A记录模块后端 - 股票交易记录CRUD与数据隔离
- 新增StockRecord实体,含userId字段实现用户数据隔离
- Controller查询自动按userId过滤,归档/恢复增加用户校验
- Service新增时自动注入当前用户ID
- 支持归档/恢复操作
2026-07-16 17:29:54 +08:00
OpenCode 1379855230 fix: 修复青龙API JSONNull序列化问题和脚本名称字段映射
- AdminQinglongController: 用Jackson ObjectMapper替代hutool JSONObject序列化,解决JSONNull问题
- qlResult方法:提取青龙API的data字段直接返回,避免前端多层嵌套
- QinglongService: sanitize方法处理JSONNull转null
2026-07-11 12:11:51 +08:00
OpenCode 6233e119d4 refactor: 青龙隔离从按用户改为按部门
- 删除 QlUserConfigEntity/Mapper/Service,新增 QlDeptConfigEntity/Mapper/Service
- QinglongService: userId → deptIds,支持多部门合并展示(mergeDataFromDepts)
- AdminQinglongController: 通过用户departmentId获取部门青龙配置
- 新增 AdminQlDeptConfigController: 部门青龙App绑定/解绑/未绑定部门列表
- BaseSysLoginServiceImpl: 移除登录时自动创建青龙App的逻辑
2026-07-11 00:11:11 +08:00
OpenCode 754b7f4782 feat: 定时任务模块 - 青龙面板集成与任务分组
- 增强 TaskInfoEntity: 新增 groupId, qlScriptId, qlScriptName, lastDuration, lastRunTime, failCount 字段
- 增强 TaskLogEntity: 新增 duration 字段
- 新增 TaskGroupEntity/mapper/service/controller: 任务分组管理
- 新增 QlUserConfigEntity/mapper/service: 每用户青龙App凭证存储
- 新增 QinglongService: 青龙Open API代理,支持每用户独立token
- 新增 AdminQinglongController: 青龙脚本/定时/环境变量API代理
- 新增 AdminTaskGroupController: 任务分组CRUD
- 修改 TaskInfoServiceImpl: 支持青龙脚本类型(taskType=2)跳过Quartz
- 修改 AdminTaskInfoController: 增加groupId过滤和关键词搜索
- 修改 BaseSysLoginServiceImpl: 登录时自动为用户创建青龙App
- 更新 application.yml/application-prod.yml: 青龙面板配置
- 更新菜单和数据库种子数据
2026-07-10 20:40:25 +08:00
OpenCode 34e224c922 feat: 部门和角色添加启用/停用状态,停用角色不参与权限计算 2026-07-10 10:44:17 +08:00
OpenCode fdf88ffa8a feat: 部门删除前校验是否有用户,有用户则拒绝删除 2026-07-09 17:21:39 +08:00
OpenCode 5116128355 feat: 添加验证码开关配置,适配Docker部署(prod连接mysql/redis) 2026-07-08 16:24:26 +08:00
ruying408 14191d2720 fix: autotable 扫描包问题 2025-09-16 13:45:08 +08:00
ruying408 1354b27010 feat: 新增查询接口入参转换方法 2025-09-15 23:55:52 +08:00
ruying408 f0f278c2e9 feat: 新增查询接口入参转换方法 2025-09-15 22:53:00 +08:00
ruying408 405472a9b2 upgrade: 1、升级autotable、mybatisflex 到最新版本 2、移除自动生成的实体代码 2025-09-13 00:44:42 +08:00
ruying408 91b26375ef upgrade: 1、升级autotable、mybatisflex 到最新版本 2、移除自动生成的实体代码 2025-09-12 21:09:26 +08:00
ruying408 24011ae9ae fix:组织架构部门查询人员问题 2025-09-11 23:41:26 +08:00
ruying408 6c1f44a565 update:查询条件按类型转换支持pg 2025-05-12 20:15:01 +08:00
ruying408 509e2f1f4d upgrade:
1、R 返回值去掉 dataMap
2、basecontroller 插入和 批量插入都使用 id返回值
3、优化postgres支持 修改 com.mybatisflex.core.handler 为 com.cool.core.mybatis.handler
2025-05-11 21:04:45 +08:00
ruying408 fc2a06d4b6 fix: eps生成修复 2025-04-30 11:49:57 +08:00
ruying408 4c18020fd7 fix: eps生成修复 2025-04-30 11:48:41 +08:00
ruying408 a39a4ea772 Merge remote-tracking branch 'origin/main' into dev 2025-04-29 23:44:17 +08:00
ruying408 2d5bc7097d Merge pull request #13 from ximu-tao/dev
通用 API 命名封装、admin 端组件生成定制
2025-04-29 23:38:45 +08:00
ruying408 7fde7645d6 Merge pull request #14 from VergLsm/dev
feat: 新增多个字段模查询,兼容旧逻辑
2025-04-29 23:38:08 +08:00
VergLsm ee695de3de feat: 新增多个字段模查询,兼容旧逻辑 2025-04-29 14:42:40 +08:00
ximu-tao a990bba61d fix 管理员信息响应数据类型问题 2025-04-24 15:01:58 +08:00
ximu-tao e21e5675f2 fix 管理员信息响应数据类型问题 2025-04-23 16:17:51 +08:00
ruying408 5d18f8bf0e update:修改readme 2025-04-20 17:22:25 +08:00
ruying408 2c2b9473a4 update:修改readme 2025-04-20 17:20:25 +08:00
ximu-tao c86c52225a EPS 增加数据结构名称信息 2025-04-19 18:46:27 +08:00
ximu-tao 7e751ef07f 前后台文档分组 2025-04-19 15:21:11 +08:00
ximu-tao 7be1be932b admin 端组件生成定制 2025-04-19 10:59:29 +08:00
ximu-tao c4f1df9774 通用 API 命名封装 2025-04-19 10:59:29 +08:00
ruying408 94b2c45eb1 fix:生成eps问题 2025-04-19 01:25:07 +08:00
ruying408 eab1b92be6 修改版本 2025-04-13 10:55:34 +08:00
ruying408 c0ff69011f upgrade:多租户 2025-04-13 00:08:58 +08:00
ruying408 bf8c6d711e upgrade:国际化多语言 2025-04-12 23:26:42 +08:00
ruying408 f9767320d6 Merge pull request #12 from ximu-tao/dev
响应类型封装优化
2025-04-12 21:18:05 +08:00
ruying408 842a6e2383 编译生成代码 2025-04-10 00:06:25 +08:00
ruying408 98109e98d2 upgrade:升级autoable版本,移除自定义数据库类型转换 2025-03-29 15:55:54 +08:00
ruying408 25a87cca86 upgrade:升级autoable版本 2025-03-29 14:46:06 +08:00
ruying408 77749e6c2e upgrade:升级8.0适配 2025-03-29 14:37:20 +08:00
ximu-tao 72e45dc2c7 响应类型封装优化 2025-03-22 23:43:14 +08:00
246 changed files with 8297 additions and 1734 deletions
+7
View File
@@ -0,0 +1,7 @@
.git
.idea
*.iws
*.iml
*.ipr
.vscode
.DS_Store
+6 -8
View File
@@ -1,14 +1,12 @@
# 使用 GraalVM 17 作为基础镜像
FROM ghcr.io/graalvm/graalvm-ce:latest
FROM eclipse-temurin:21-jre
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 设置容器内的工作目录
WORKDIR /app
# 将可执行的jar文件复制到容器内
COPY target/cool-admin-7.1.0.jar /app/cool-admin-7.1.0.jar
COPY target/cool-admin-8.0.0.jar /app/cool-admin-8.0.0.jar
# 暴露Spring Boot应用程序运行的端口
EXPOSE 8001
# 运行Spring Boot应用程序的命令
ENTRYPOINT ["java", "-jar", "/app/cool-admin-7.1.0.jar", "--spring.profiles.active=prod"]
ENTRYPOINT ["java", "-jar", "/app/cool-admin-8.0.0.jar", "--spring.profiles.active=prod"]
+6 -3
View File
@@ -19,12 +19,15 @@
Ai时代,很多老旧的框架已经无法满足现代化的开发需求,Cool-Admin开发了一系列的功能,让开发变得更简单、更快速、更高效。
- **Ai编码**:通过微调大模型学习框架特有写法,实现简单功能从Api接口到前端页面的一键生成
- **流程编排**:通过拖拽编排方式,即可实现类似像智能客服这样的功能
- **Ai编码**:通过微调大模型学习框架特有写法,实现简单功能从Api接口到前端页面的一键生成[详情](https://java.cool-admin.com/src/guide/ai.html)
- **流程编排**:通过拖拽编排方式,即可实现类似像智能客服这样的功能[详情](https://cool-js.com/plugin/118)
- **多租户**:支持多租户,采用全局动态注入查询条件[详情](https://java.cool-admin.com/src/guide/tenant.html)
- **多语言**:基于大模型自动翻译,无需更改原有代码[详情](https://java.cool-admin.com/src/guide/i18n.html)
- **模块化**:代码是模块化的,清晰明了,方便维护
- **插件化**:插件化的设计,可以通过安装插件的方式扩展如:支付、短信、邮件等功能
- **自动初始化**:数据自动化,无需再手动维护,启动时自动生成数据库表和表结构数据
- **cool-admin-java-plus**: [cool-admin-java-plus](https://gitee.com/hlc4417/cool-admin-java-plus)
- **cool-admin-java-plus**: [详情](https://gitee.com/hlc4417/cool-admin-java-plus)
- ......
![](https://cool-show.oss-cn-shanghai.aliyuncs.com/admin/flow.png)
## 地址
+8 -5
View File
@@ -12,20 +12,20 @@
</parent>
<groupId>com.cool</groupId>
<artifactId>cool-admin</artifactId>
<version>7.1.0</version>
<version>8.0.0</version>
<name>cool-admin</name>
<description>cool admin for java</description>
<properties>
<java.version>17</java.version>
<lombok.version>1.18.34</lombok.version>
<mybatis-flex.version>1.10.8</mybatis-flex.version>
<mybatis-flex.ext.version>1.10.8.110</mybatis-flex.ext.version>
<mybatis-flex.version>1.11.0</mybatis-flex.version>
<mybatis-flex.ext.version>1.11.0.146</mybatis-flex.ext.version>
<hutool.version>5.8.26</hutool.version>
<ognl.version>3.3.2</ognl.version>
<fastjson2.version>2.0.51</fastjson2.version>
<springdoc-openapi.version>2.5.0</springdoc-openapi.version>
<perf4j.version>0.9.16</perf4j.version>
<weixin-java.version>4.6.3.B</weixin-java.version>
<weixin-java.version>4.7.0</weixin-java.version>
</properties>
<dependencies>
@@ -71,6 +71,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- springframework end -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
@@ -84,7 +88,6 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
@@ -0,0 +1,80 @@
package com.cool.core.annotation;
import org.dromara.autotable.annotation.ColumnComment;
import org.dromara.autotable.annotation.ColumnDefault;
import org.dromara.autotable.annotation.ColumnNotNull;
import org.dromara.autotable.annotation.ColumnType;
import org.dromara.autotable.annotation.enums.DefaultValueEnum;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ColumnType
@ColumnNotNull
@ColumnDefault
@ColumnComment("")
public @interface ColumnDefine {
/**
* 字段类型:不填默认使用属性的数据类型进行转换,转换失败的字段不会添加
*
* @return 字段类型
*/
@AliasFor(annotation = ColumnType.class, attribute = "value")
String type() default "";
/**
* 字段长度,默认是-1,小于0相当于null
*
* @return 默认字段长度
*/
@AliasFor(annotation = ColumnType.class, attribute = "length")
int length() default -1;
/**
* 小数点长度,默认是-1,小于0相当于null
*
* @return 小数点长度
*/
@AliasFor(annotation = ColumnType.class, attribute = "decimalLength")
int decimalLength() default -1;
/**
* 是否为可以为null,true是可以,false是不可以,默认为true
*
* @return 是否为可以为null,true是不可以,false是可以,默认为false
*/
@AliasFor(annotation = ColumnNotNull.class, attribute = "value")
boolean notNull() default false;
/**
* 默认值,默认为null
*
* @return 默认值
*/
@AliasFor(annotation = ColumnDefault.class, attribute = "type")
DefaultValueEnum defaultValueType() default DefaultValueEnum.UNDEFINED;
/**
* 默认值,默认为null
*
* @return 默认值
*/
@AliasFor(annotation = ColumnDefault.class, attribute = "value")
String defaultValue() default "";
/**
* 数据表字段备注
*
* @return 默认值,默认为空
*/
@AliasFor(annotation = ColumnComment.class, attribute = "value")
String comment() default "";
}
@@ -0,0 +1,14 @@
package com.cool.core.annotation;
import com.cool.core.enums.AdminComponentsEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EpsField {
String component() default AdminComponentsEnum.INPUT;
}
@@ -1,53 +0,0 @@
package com.cool.core.autotable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import org.dromara.autotable.annotation.ColumnType;
import org.dromara.autotable.core.constants.DatabaseDialect;
import org.dromara.autotable.core.converter.DatabaseTypeAndLength;
import org.dromara.autotable.core.converter.JavaTypeToDatabaseTypeConverter;
import org.dromara.autotable.core.strategy.pgsql.data.PgsqlDefaultTypeEnum;
import org.dromara.autotable.core.utils.StringUtils;
import org.dromara.autotable.core.utils.TableBeanUtils;
import org.springframework.stereotype.Component;
@Component
public class CustomJavaTypeToDatabaseTypeConverter implements JavaTypeToDatabaseTypeConverter {
/**
* 重新 java转数据库类型
* 兼容 mybatisflex 不兼容 postgresql 的json类型问题
*/
@Override
public DatabaseTypeAndLength convert(String databaseDialect, Class<?> clazz, Field field) {
ColumnType column = TableBeanUtils.getColumnType(field);
// 设置了类型
if (column != null) {
String type = column.value();
Integer length = column.length() > -1 ? column.length() : null;
Integer decimalLength = column.decimalLength() > -1 ? column.decimalLength() : null;
List<String> values = Arrays.asList(column.values());
// 如果明确指定了类型名,直接替换
if (StringUtils.hasText(type)) {
if (DatabaseDialect.PostgreSQL.equals(databaseDialect) &&
type.equalsIgnoreCase("json")) {
// json 类型在 postgresql 处理成 text
PgsqlDefaultTypeEnum typeEnum = PgsqlDefaultTypeEnum.TEXT;
return new DatabaseTypeAndLength(typeEnum.getTypeName(), typeEnum.getDefaultLength(), typeEnum.getDefaultLength(), values);
}
return new DatabaseTypeAndLength(type, length, decimalLength, values);
}
// 如果没有指定明确的类型名,但是却指定了长度。那么使用默认类型+指定长度
if (length != null || decimalLength != null) {
DatabaseTypeAndLength typeAndLength = getDatabaseTypeAndLength(databaseDialect, clazz, field);
typeAndLength.setLength(length);
typeAndLength.setDecimalLength(decimalLength);
return typeAndLength;
}
}
// 其他情况,使用默认类型
return getDatabaseTypeAndLength(databaseDialect, clazz, field);
}
}
@@ -51,9 +51,11 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
protected final String COOL_PAGE_OP = "COOL_PAGE_OP";
protected final String COOL_LIST_OP = "COOL_LIST_OP";
protected final String COOL_INFO_OP = "COOL_INFO_OP";
private final ThreadLocal<CrudOption<T>> pageOption = new ThreadLocal<>();
private final ThreadLocal<CrudOption<T>> listOption = new ThreadLocal<>();
private final ThreadLocal<CrudOption<T>> infoOption = new ThreadLocal<>();
private final ThreadLocal<JSONObject> requestParams = new ThreadLocal<>();
@ModelAttribute
@@ -61,16 +63,20 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
@RequestAttribute JSONObject requestParams) {
String requestPath = ((ServletRequestAttributes) Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest().getRequestURI();
if (!requestPath.endsWith("/page") && !requestPath.endsWith("/list")) {
if (!requestPath.endsWith("/page") && !requestPath.endsWith("/list")
&& !requestPath.endsWith("/info")) {
// 非page或list不执行
return;
}
this.pageOption.set(new CrudOption<>(requestParams));
this.listOption.set(new CrudOption<>(requestParams));
this.infoOption.set(new CrudOption<>(requestParams));
this.requestParams.set(requestParams);
init(request, requestParams);
request.setAttribute(COOL_PAGE_OP, this.pageOption.get());
request.setAttribute(COOL_LIST_OP, this.listOption.get());
request.setAttribute(COOL_INFO_OP, this.infoOption.get());
removeThreadLocal();
}
@@ -87,6 +93,10 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
return new CrudOption<>(this.requestParams.get());
}
public void setInfoOption(CrudOption<T> infoOption) {
this.infoOption.set(infoOption);
}
public void setListOption(CrudOption<T> listOption) {
this.listOption.set(listOption);
}
@@ -105,11 +115,12 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
@Operation(summary = "新增", description = "新增信息,对应后端的实体类")
@PostMapping("/add")
protected R add(@RequestAttribute() JSONObject requestParams) {
checkPermission("add");
String body = requestParams.getStr("body");
if (JSONUtil.isTypeJSONArray(body)) {
JSONArray array = JSONUtil.parseArray(body);
return R.ok(Dict.create()
.set("ids", service.addBatch(requestParams, array.toList(currentEntityClass()))));
.set("id", service.addBatch(requestParams, array.toList(currentEntityClass()))));
} else {
return R.ok(Dict.create().set("id",
service.add(requestParams, requestParams.toBean(currentEntityClass()))));
@@ -125,6 +136,7 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
@PostMapping("/delete")
protected R delete(HttpServletRequest request, @RequestBody Map<String, Object> params,
@RequestAttribute() JSONObject requestParams) {
checkPermission("delete");
service.delete(requestParams, Convert.toLongArray(getIds(params)));
return R.ok();
}
@@ -137,6 +149,7 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
@Operation(summary = "修改", description = "根据ID修改")
@PostMapping("/update")
protected R update(@RequestBody T t, @RequestAttribute() JSONObject requestParams) {
checkPermission("update");
Long id = t.getId();
JSONObject info = JSONUtil.parseObj(JSONUtil.toJsonStr(service.getById(id)));
requestParams.forEach(info::set);
@@ -152,9 +165,13 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
*/
@Operation(summary = "信息", description = "根据ID查询单个信息")
@GetMapping("/info")
protected R info(@RequestAttribute() JSONObject requestParams,
@RequestParam() Long id) {
return R.ok(service.info(requestParams, id));
protected R<T> info(@RequestAttribute() JSONObject requestParams,
@RequestParam() Long id,
@RequestAttribute(COOL_INFO_OP) CrudOption<T> option) {
invokerTransformParam(option, requestParams);
T info = (T) service.info(requestParams, id);
invokerTransformValue(option, info);
return R.ok(info);
}
/**
@@ -164,15 +181,16 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
*/
@Operation(summary = "查询", description = "查询多个信息")
@PostMapping("/list")
protected R list(@RequestAttribute() JSONObject requestParams,
protected R<List<T>> list(@RequestAttribute() JSONObject requestParams,
@RequestAttribute(COOL_LIST_OP) CrudOption<T> option) {
invokerTransformParam(option, requestParams);
QueryModeEnum queryModeEnum = option.getQueryModeEnum();
List list = (List) switch (queryModeEnum) {
case ENTITY_WITH_RELATIONS -> service.listWithRelations(requestParams, option.getQueryWrapper(currentEntityClass()));
case CUSTOM -> transformList(service.list(requestParams, option.getQueryWrapper(currentEntityClass()), option.getAsType()), option.getAsType());
default -> service.list(requestParams, option.getQueryWrapper(currentEntityClass()));
};
invokerTransform(option, list);
invokerTransformValue(option, list);
return R.ok(list);
}
@@ -183,8 +201,9 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
*/
@Operation(summary = "分页", description = "分页查询多个信息")
@PostMapping("/page")
protected R page(@RequestAttribute() JSONObject requestParams,
protected R<PageResult<T>> page(@RequestAttribute() JSONObject requestParams,
@RequestAttribute(COOL_PAGE_OP) CrudOption<T> option) {
invokerTransformParam(option, requestParams);
Integer page = requestParams.getInt("page", 1);
Integer size = requestParams.getInt("size", 20);
QueryModeEnum queryModeEnum = option.getQueryModeEnum();
@@ -194,39 +213,41 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
default -> service.page(requestParams, new Page<>(page, size), option.getQueryWrapper(currentEntityClass()));
};
Page pageResult = (Page) obj;
invokerTransform(option, pageResult.getRecords());
invokerTransformValue(option, pageResult.getRecords());
return R.ok(pageResult(pageResult));
}
/**
* 转换参数,组装数据
* 转换值,组装数据
*/
private void invokerTransform(CrudOption<T> option, Object obj) {
if (ObjUtil.isNotEmpty(option.getTransform())) {
private void invokerTransformValue(CrudOption<T> option, Object obj) {
if (ObjUtil.isNotNull(option.getTransformValue())) {
if (obj instanceof List) {
((List)obj).forEach(o -> {
option.getTransform().apply(o);
option.getTransformValue().apply(o);
});
} else {
option.getTransform().apply(obj);
option.getTransformValue().apply(obj);
}
}
}
/**
* 转换入参
*/
private void invokerTransformParam(CrudOption<T> option, JSONObject obj) {
if (ObjUtil.isNotNull(option.getTransformParam())) {
option.getTransformParam().apply(obj);
}
}
/**
* 分页结果
*
* @param page 分页返回数据
*/
protected PageResult pageResult(Page page) {
PageResult result = new PageResult();
Map<String, Object> pagination = new HashMap<>();
pagination.put("size", page.getPageSize());
pagination.put("page", page.getPageNumber());
pagination.put("total", page.getTotalRow());
result.setList(page.getRecords());
result.setPagination(pagination);
return result;
protected PageResult<T> pageResult(Page<T> page) {
return PageResult.of(page);
}
public Class<T> currentEntityClass() {
@@ -268,4 +289,7 @@ public abstract class BaseController<S extends BaseService<T>, T extends BaseEnt
page.setRecords(transformList(page.getRecords(), asType));
return page;
}
protected void checkPermission(String action) {
}
}
@@ -6,7 +6,7 @@ import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.core.activerecord.Model;
import com.mybatisflex.core.query.QueryWrapper;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import java.io.Serializable;
import java.util.Date;
import lombok.Getter;
@@ -31,6 +31,10 @@ public abstract class BaseEntity<T extends Model<T>> extends Model<T> implements
@ColumnDefine(comment = "更新时间")
protected Date updateTime;
@Column(isLogicDelete = true)
@ColumnDefine(comment = "删除标记 0:未删除 1:已删除", defaultValue = "0")
protected Integer delFlag;
@Ignore
@Column(ignore = true)
@JsonIgnore
@@ -0,0 +1,16 @@
package com.cool.core.base;
import com.mybatisflex.core.activerecord.Model;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
/** 租户ID实体类 */
@Getter
@Setter
public class TenantEntity<T extends Model<T>> extends BaseEntity<T> {
@Index
@ColumnDefine(comment = "租户id")
protected Long tenantId;
}
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ser.std.NumberSerializer;
import java.io.IOException;
import java.math.BigInteger;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
@@ -29,6 +30,7 @@ public class JacksonConfig {
objectMapper.registerModule(simpleModule);
// 配置日期格式为 yyyy-MM-dd HH:mm:ss
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
objectMapper.setDateFormat(dateFormat);
return new MappingJackson2HttpMessageConverter(objectMapper);
}
@@ -1,7 +1,11 @@
package com.cool.core.config;
import com.cool.core.tenant.CoolTenantFactory;
import com.mybatisflex.core.FlexGlobalConfig;
import com.mybatisflex.core.tenant.TenantFactory;
import com.mybatisflex.spring.boot.MyBatisFlexCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@@ -10,6 +14,14 @@ public class MyBatisFlexConfiguration implements MyBatisFlexCustomizer {
@Override
public void customize(FlexGlobalConfig globalConfig) {
// 我们可以在这里进行一些列的初始化配置
// 指定多租户列的列名
FlexGlobalConfig.getDefaultConfig().setTenantColumn("tenant_id");
}
@Bean
@ConditionalOnProperty(name = "cool.multi-tenant.enable", havingValue = "true")
public TenantFactory tenantFactory(){
return new CoolTenantFactory();
}
}
@@ -0,0 +1,105 @@
package com.cool.core.enums;
public class AdminComponentsEnum {
/**
* 省市区选择器 - 用户选择省市区
*/
public static final String PCA = "pca";
/**
* 文本输入 - 文本编辑框
*/
public static final String INPUT = "input";
/**
* 文本域 - 多行文本编辑框
*/
public static final String TEXTAREA = "textarea";
/**
* 富文本编辑器 - 用于文章,商品详情的编辑
*/
public static final String EDITOR_RICH = "editor-rich";
/**
* 代码编辑器 - 用于开发人员编写代码,支持多种语言,支持代码高亮,支持代码格式化
*/
public static final String CODING = "coding";
/**
* 数字输入 - 数字输入编辑框
*/
public static final String INPUT_NUMBER = "input-number";
/**
* 日期选择器 - 用户选择 年-月-日
*/
public static final String DATE = "date";
/**
* 日期范围选择器 - 用户选择起始 年-月-日
*/
public static final String DATERANGE = "daterange";
/**
* 时间选择器 - 用户选择 时:分:秒
*/
public static final String DATETIME = "datetime";
/**
* 时间范围选择器 - 用户选择起始 年-月-日 时:分:秒
*/
public static final String DATETIMERANGE = "datetimerange";
/**
* 单图上传 - 用户上传单张图片,如:头像、logo、封面
*/
public static final String UPLOAD_IMG = "upload-img";
/**
* 多图上传 - 用户上传多张图片, 如:照片、图片
*/
public static final String UPLOAD_IMG_MULTIPLE = "upload-img-multiple";
/**
* 单个文件上传 - 用户上传单个文件
*/
public static final String UPLOAD_FILE = "upload-file";
/**
* 多个文件上传 - 用户上传多个文件
*/
public static final String UPLOAD_FILE_MULTIPLE = "upload-file-multiple";
/**
* 状态选择器 - 用户开启或者关闭操作,如:是否启用、是否推荐、是否默认、置顶、启用禁用等
*/
public static final String SWITCH = "switch";
/**
* 评分选择器 - 用户评分
*/
public static final String RATE = "rate";
/**
* 滑块选择器 - 在一个固定区间内进行选择, 如:进度
*/
public static final String PROGRESS = "progress";
/**
* 单选框 - 在一组备选项中进行单选,如:审批状态
*/
public static final String RADIO = "radio";
/**
* 多选框 - 适用于选项比较少的情况,在一组备选项中进行多选, 如:学历、爱好等
*/
public static final String CHECKBOX = "checkbox";
/**
* 下拉框 - 适用于当选项过多时,使用下拉菜单展示并选择内容,如:分类、标签等
*/
public static final String SELECT = "select";
}
@@ -0,0 +1,13 @@
package com.cool.core.enums;
public class Apis {
public static final String ADD = "add";
public static final String DELETE = "delete";
public static final String UPDATE = "update";
public static final String PAGE = "page";
public static final String LIST = "list";
public static final String INFO = "info";
public static final String[] ALL_API = new String[]{ ADD, DELETE, UPDATE, PAGE, LIST, INFO };
}
+71 -17
View File
@@ -10,9 +10,11 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.core.annotation.EpsField;
import com.cool.core.annotation.TokenIgnore;
import com.cool.core.config.CustomOpenApiResource;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
@@ -43,6 +45,8 @@ public class CoolEps {
private int serverPort;
private Dict entityInfo;
private Dict menuInfo;
private JSONObject swaggerInfo;
@@ -62,6 +66,7 @@ public class CoolEps {
return;
}
entityInfo = Dict.create();
menuInfo = Dict.create();
swaggerInfo = swaggerInfo();
Runnable task = () -> {
entity();
@@ -86,25 +91,28 @@ public class CoolEps {
private void urls() {
Dict admin = Dict.create();
Dict app = Dict.create();
ArrayList<Object> emptyList = new ArrayList<>();
Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> methodEntry : map.entrySet()) {
RequestMappingInfo info = methodEntry.getKey();
HandlerMethod method = methodEntry.getValue();
TokenIgnore tokenIgnore = method.getMethodAnnotation(TokenIgnore.class);
String module = getModule(method);
if (StrUtil.isNotEmpty(module)) {
String entityName = getEntity(method.getBeanType());
String methodPath = getMethodUrl(method);
String escapedMethodPath = methodPath.replace("{", "\\{").replace("}", "\\}");
String prefix = Objects.requireNonNull(getUrl(info))
.replaceFirst("(?s)(.*)" + escapedMethodPath, "$1");
Dict result = Dict.create();
.replaceFirst("(?s)(.*)" + escapedMethodPath, "$1");
Dict result;
int type = 0;
if (prefix.startsWith("/admin")) {
result = admin;
}
if (prefix.startsWith("/app")) {
} else if (prefix.startsWith("/app")) {
result = app;
type = 1;
} else {
continue;
}
if (result.get(module) == null) {
result.set(module, new ArrayList<Dict>());
@@ -121,16 +129,24 @@ public class CoolEps {
}
});
if (item != null) {
item.set("api", apis(prefix, methodPath, item.getBean("api")));
item.set("api", apis(prefix, methodPath, item.getBean("api"), tokenIgnore));
} else {
item = Dict.create();
item.set("controller", method.getBeanType().getSimpleName());
item.set("module", module);
item.set("info", Dict.create().set("type",
Dict.create()
.set("name", getLastPathSegment(prefix))
.set("description", "")
));
item.set("api", apis(prefix, methodPath, item.getBean("api"), tokenIgnore));
item.set("name", entityName);
item.set("api", new ArrayList<Dict>());
item.set("prefix", prefix);
item.set("columns", entityInfo.get(entityName));
item.set("api", apis(prefix, methodPath, item.getBean("api")));
item.set("pageQueryOp", Dict.create().set("keyWordLikeFields", emptyList)
.set("fieldEq", emptyList)
.set("fieldLike", emptyList));
item.set("prefix", prefix);
item.set("menu", menuInfo.get(entityName));
urls.add(item);
}
if (type == 0) {
@@ -146,6 +162,28 @@ public class CoolEps {
this.app = app;
}
/**
* 提取URL路径中的最后一个路径段
* 示例:输入 "/api/getData" 返回 "getData"
*/
private String getLastPathSegment(String url) {
if (StrUtil.isBlank(url)) {
return "";
}
int queryIndex = url.indexOf('?');
if (queryIndex != -1) {
url = url.substring(0, queryIndex);
}
int slashIndex = url.lastIndexOf('/');
if (slashIndex != -1 && slashIndex < url.length() - 1) {
return url.substring(slashIndex + 1);
} else {
return url;
}
}
/**
* 设置所有的api
@@ -155,13 +193,18 @@ public class CoolEps {
* @param list api列表
* @return api列表
*/
private List<Dict> apis(String prefix, String methodPath, List<Dict> list) {
private List<Dict> apis(String prefix, String methodPath, List<Dict> list, TokenIgnore tokenIgnore) {
if (ObjUtil.isNull(list)) {
list = new ArrayList<>();
}
Dict item = Dict.create();
item.set("method", "");
item.set("path", methodPath);
item.set("summary", "");
item.set("tag", "");
item.set("dts", new Object());
item.set("dts", Dict.create());
item.set("ignoreToken", false);
if (tokenIgnore != null) {
item.set("ignoreToken", true);
}
setSwaggerInfo(item, prefix + methodPath);
list.add(item);
return list;
@@ -178,10 +221,8 @@ public class CoolEps {
JSONObject urlInfo = paths.getJSONObject(url);
String method = urlInfo.keySet().iterator().next();
JSONObject methodInfo = urlInfo.getJSONObject(method);
item.set("dts", methodInfo);
item.set("method", method);
item.set("summary", methodInfo.getStr("summary"));
item.set("description", methodInfo.get("description"));
}
/**
@@ -284,6 +325,13 @@ public class CoolEps {
Field[] fields = getAllDeclaredFields(e);
List<Dict> columns = columns(fields);
entityInfo.set(e.getSimpleName(), columns);
Table mergedAnnotation = AnnotatedElementUtils.findMergedAnnotation(e, Table.class);
menuInfo.set(e.getSimpleName(), mergedAnnotation.comment());
});
}
@@ -323,8 +371,13 @@ public class CoolEps {
List<Dict> dictList = new ArrayList<>();
for (Field field : fields) {
Dict dict = Dict.create();
ColumnDefine columnInfo = AnnotatedElementUtils.findMergedAnnotation(field,
ColumnDefine.class);
EpsField epsField = AnnotatedElementUtils.findMergedAnnotation(field, EpsField.class);
if (epsField != null) {
dict.set("component", epsField.component());
}
ColumnDefine columnInfo = AnnotatedElementUtils.findMergedAnnotation(field, ColumnDefine.class);
if (columnInfo == null) {
continue;
}
@@ -333,6 +386,7 @@ public class CoolEps {
dict.set("propertyName", field.getName());
dict.set("type", matchType(field.getType().getName()));
dict.set("nullable", !columnInfo.notNull());
dict.set("source", "a." + field.getName());
dictList.add(dict);
}
return dictList;
@@ -21,10 +21,10 @@ public class CoolExceptionHandler {
public R handleRRException(CoolException e) {
R r = new R();
if (ObjUtil.isNotEmpty(e.getData())) {
r.put("data", e.getData());
r.setData( e.getData() );
} else {
r.put("code", e.getCode());
r.put("message", e.getMessage());
r.setCode( e.getCode() );
r.setMessage( e.getMessage() );
}
if (ObjUtil.isNotEmpty(e.getCause())) {
log.error(e.getCause().getMessage(), e.getCause());
@@ -1,6 +1,8 @@
package com.cool.core.exception;
import cn.hutool.core.util.ObjectUtil;
import com.cool.core.request.R;
import com.cool.core.util.I18nUtil;
import java.util.Arrays;
import java.util.Optional;
import lombok.Getter;
@@ -57,6 +59,13 @@ public class CoolPreconditions {
returnData(true, data);
}
public static void returnNoData(Object object) {
if (ObjectUtil.isEmpty(object)) {
R r = R.ok();
throw new CoolException(r.getMessage(), r.getCode());
}
}
/**
* 对象如果为空 就抛异常
*/
@@ -69,6 +78,7 @@ public class CoolPreconditions {
}
private static String formatMessage(String messagePattern, Object... arguments) {
messagePattern = I18nUtil.getI18nMsg(messagePattern);
StringBuilder sb = new StringBuilder();
int argumentIndex = 0;
int placeholderIndex = messagePattern.indexOf("{}");
@@ -0,0 +1,219 @@
package com.cool.core.i18n;
import static com.cool.core.util.I18nUtil.*;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.core.lock.CoolLock;
import com.cool.core.util.CoolPluginInvokers;
import com.cool.core.util.I18nUtil;
import com.cool.core.util.PathUtils;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.service.sys.BaseSysMenuService;
import com.cool.modules.dict.entity.DictInfoEntity;
import com.cool.modules.dict.entity.DictTypeEntity;
import com.cool.modules.dict.service.DictInfoService;
import com.cool.modules.dict.service.DictTypeService;
import com.mybatisflex.core.query.QueryWrapper;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class I18nGenerator {
private final BaseSysMenuService baseSysMenuService;
private final DictTypeService dictTypeService;
private final DictInfoService dictInfoService;
private final CoolLock coolLock;
private final I18nUtil i18nUtil;
private List<String> languages;
private static final Duration DURATION = Duration.ofSeconds(30);
public void run(Map<String, Object> map) {
log.info("国际化 翻译...");
languages = (List<String>) map.getOrDefault("languages", List.of("zh-cn", "zh-tw", "en"));
path = (String) map.getOrDefault("path", "assets/i18n");
init();
log.info("✅国际化 翻译 成功!!!");
enable = true;
}
public void init() {
// 四个任务并发执行
CompletableFuture<Void> futureMsg = CompletableFuture.runAsync(this::genBaseMsg);
CompletableFuture<Void> futureMenu = CompletableFuture.runAsync(this::genBaseMenu);
CompletableFuture<Void> futureDictInfo = CompletableFuture.runAsync(this::genBaseDictInfo);
CompletableFuture<Void> futureDictType = CompletableFuture.runAsync(this::genBaseDictType);
// 等待全部执行完成
CompletableFuture.allOf(futureMsg, futureMenu, futureDictInfo, futureDictType).join();
}
private void genBaseMsg() {
try {
Map<String, String> msgMap = new HashMap<>();
// 从idea本地启动时,从项目目录中读取
Files.walk(Paths.get(System.getProperty("user.dir")))
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".java"))
.filter(path -> !path.toString().contains("/target/") && !path.toString().contains("/.git/"))
.forEach(path -> msgMap.putAll(processFile(path)));
if (ObjUtil.isNotEmpty(msgMap)) {
// 系统异常信息,输出到resources/i18n 文件夹下,只有本地运行会生成
File msgfile = FileUtil.file(PathUtils.getUserDir(),
"src", "main", "resources", "cool", "i18n", "msg", "template.json");
// 确保父目录存在
FileUtil.mkParentDirs(msgfile);
// 写入内容
FileUtil.writeUtf8String(JSONUtil.toJsonStr(msgMap), msgfile);
} else {
try {
// jar启动时,从jar包中读取
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource resource = resolver.getResource("classpath:cool/i18n/msg/template.json");
String content = FileUtil.readUtf8String(resource.getFile());
msgMap.putAll(JSONUtil.toBean(content, Map.class));
} catch (Exception e) {
log.error("获取系统异常信息失败", e);
}
}
extracted(MSG_PREFIX, msgMap);
} catch (Exception e) {
log.error("国际化系统异常信息失败", e);
}
}
/**
* 生成菜单信息国际化
*/
@Async
public void asyncGenBaseMenu() {
if (coolLock.tryLock(MENU_PREFIX, DURATION)) {
genBaseMenu();
coolLock.unlock(MENU_PREFIX);
}
}
private void genBaseMenu() {
try {
Map<String, String> menuMap = baseSysMenuService.list(QueryWrapper.create().select(BaseSysMenuEntity::getName))
.stream()
.collect(Collectors.toMap(
BaseSysMenuEntity::getName,
BaseSysMenuEntity::getName,
(oldValue, newValue) -> oldValue
));
extracted(MENU_PREFIX, menuMap);
} catch (Exception e) {
log.error("国际化菜单信息失败", e);
}
}
@Async
public void asyncGenBaseDictType() {
if (coolLock.tryLock(DICT_TYPE_PREFIX, DURATION)) {
genBaseDictType();
coolLock.unlock(DICT_TYPE_PREFIX);
}
}
private void genBaseDictType() {
try {
Map<String, String> dataMap = dictTypeService.list(QueryWrapper.create().select(DictTypeEntity::getName))
.stream()
.collect(Collectors.toMap(
DictTypeEntity::getName,
DictTypeEntity::getName,
(oldValue, newValue) -> oldValue
));
extracted(DICT_TYPE_PREFIX, dataMap);
} catch (Exception e) {
log.error("国际化字段类型信息失败", e);
}
}
@Async
public void asyncGenBaseDictInfo() {
if (coolLock.tryLock(DICT_INFO_PREFIX, DURATION)) {
genBaseDictInfo();
coolLock.unlock(DICT_INFO_PREFIX);
}
}
private void genBaseDictInfo() {
try {
Map<String, String> dataMap = dictInfoService.list(QueryWrapper.create().select(DictInfoEntity::getName))
.stream()
.collect(Collectors.toMap(
DictInfoEntity::getName,
DictInfoEntity::getName,
(oldValue, newValue) -> oldValue
));
extracted(DICT_INFO_PREFIX, dataMap);
} catch (Exception e) {
log.error("国际化字段类型信息失败", e);
}
}
private void extracted(String prefix, Map<String, String> dataMap) {
languages.forEach(language -> {
String key = prefix + language;
if (!i18nUtil.exist(key)) {
JSONObject jsonObject = invokeTranslate(dataMap, language);
if (ObjUtil.isNotNull(jsonObject)) {
i18nUtil.update(key, jsonObject);
}
}
});
}
private JSONObject invokeTranslate(Map<String, String> map, String language) {
return (JSONObject) CoolPluginInvokers.invoke("i18n", "invokeTranslate", map, language);
}
// 匹配 CoolPreconditions 抛异常语句中的中文字符串
private static final Pattern EXCEPTION_PATTERN = Pattern.compile(
"CoolPreconditions\\.(\\w+)\\s*\\([^;]*?\"([^\"]*[\u4e00-\u9fa5]+[^\"]*)\"", Pattern.MULTILINE
);
private static Map<String, String> processFile(Path path) {
Map<String, String> map = new HashMap<>();
try {
String content = Files.readString(path, StandardCharsets.UTF_8);
// 去掉注释
content = removeComments(content);
// 仅查找方法体内的 CoolPreconditions 调用
Matcher matcher = EXCEPTION_PATTERN.matcher(content);
while (matcher.find()) {
String chineseText = matcher.group(2).trim();
map.put(chineseText, chineseText);
}
} catch (IOException e) {
e.printStackTrace();
}
return map;
}
// 移除注释(单行与多行)
private static String removeComments(String code) {
String noMultiLine = code.replaceAll("/\\*.*?\\*/", ""); // 多行注释
return noMultiLine.replaceAll("//.*", ""); // 单行注释
}
}
@@ -1,5 +1,6 @@
package com.cool.core.init;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjUtil;
@@ -8,12 +9,15 @@ import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.core.base.service.MapperProviderService;
import com.cool.core.mybatis.pg.PostgresSequenceSyncService;
import com.cool.core.util.DatabaseDialectUtils;
import com.cool.core.util.EntityUtils;
import com.cool.modules.base.entity.sys.BaseSysConfEntity;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.service.sys.BaseSysConfService;
import com.cool.modules.base.service.sys.BaseSysMenuService;
import com.mybatisflex.core.BaseMapper;
import com.mybatisflex.core.query.QueryWrapper;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@@ -47,6 +51,8 @@ public class DBFromJsonInit {
final private ApplicationEventPublisher eventPublisher;
final private PostgresSequenceSyncService postgresSequenceSyncService;
@Value("${cool.initData}")
private boolean initData;
@@ -56,14 +62,24 @@ public class DBFromJsonInit {
return;
}
// 初始化自定义的数据
extractedDb();
boolean initFlag = extractedDb();
// 初始化菜单数据
extractedMenu();
initFlag = extractedMenu() || initFlag;
// 发送数据库初始化完成事件
eventPublisher.publishEvent(new DbInitCompleteEvent(this));
if (initFlag) {
// 如果是postgresql,同步序列
syncIdentitySequences();
}
log.info("数据初始化完成!");
}
private void syncIdentitySequences() {
if (DatabaseDialectUtils.isPostgresql()) {
postgresSequenceSyncService.syncIdentitySequences();
}
}
@Getter
public static class DbInitCompleteEvent {
private final Object source;
@@ -77,21 +93,23 @@ public class DBFromJsonInit {
/**
* 解析插入业务数据
*/
private void extractedDb() {
private boolean extractedDb() {
try {
// 加载 JSON 文件
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources("classpath:cool/data/db/*.json");
// 遍历所有.json文件
analysisResources(resources);
return analysisResources(resources);
} catch (Exception e) {
log.error("Failed to initialize data", e);
}
return false;
}
private void analysisResources(Resource[] resources)
private boolean analysisResources(Resource[] resources)
throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
String prefix = "db_";
boolean isInit = false;
for (Resource resource : resources) {
File resourceFile = new File(resource.getURL().getFile());
String fileName = prefix + resourceFile.getName();
@@ -110,8 +128,10 @@ public class DBFromJsonInit {
baseSysUserEntity.setCValue("success");
// 当前文件已加载
baseSysConfService.add(baseSysUserEntity);
isInit = true;
log.info("{} 业务数据初始化成功...", fileName);
}
return isInit;
}
private void analysisJson(JSONObject jsonObject)
@@ -156,7 +176,8 @@ public class DBFromJsonInit {
/**
* 解析插入菜单数据
*/
public void extractedMenu() {
public boolean extractedMenu() {
boolean initFlag = false;
try {
String prefix = "menu_";
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@@ -171,10 +192,12 @@ public class DBFromJsonInit {
continue;
}
analysisResources(resource, fileName);
initFlag = true;
}
} catch (Exception e) {
log.error("Failed to initialize data", e);
}
return initFlag;
}
private void analysisResources(Resource resource, String fileName) throws IOException {
@@ -204,7 +227,19 @@ public class DBFromJsonInit {
menuEntity.setParentName(parentMenuEntity.getName());
menuEntity.setParentId(parentMenuEntity.getId());
}
baseSysMenuService.add(menuEntity);
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(BaseSysMenuEntity::getName, menuEntity.getName());
if (ObjUtil.isNull(menuEntity.getParentId())) {
queryWrapper.isNull(BaseSysMenuEntity::getParentId);
} else {
queryWrapper.eq(BaseSysMenuEntity::getParentId, menuEntity.getParentId());
}
BaseSysMenuEntity dbBaseSysMenuEntity = baseSysMenuService.getOne(queryWrapper);
if (ObjUtil.isNull(dbBaseSysMenuEntity)) {
baseSysMenuService.add(menuEntity);
} else {
menuEntity = dbBaseSysMenuEntity;
}
// 递归处理子菜单
JSONArray childMenus = jsonObj.getJSONArray("childMenus");
if (childMenus != null) {
@@ -2,7 +2,7 @@ package com.cool.core.leaf.segment.entity;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import com.tangzc.mybatisflex.autotable.annotation.UniIndex;
import lombok.Getter;
import lombok.Setter;
@@ -0,0 +1,48 @@
package com.cool.core.mybatis.handler;
import com.cool.core.util.DatabaseDialectUtils;
import com.mybatisflex.core.util.StringUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.postgresql.util.PGobject;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public abstract class BaseJsonTypeHandler<T> extends BaseTypeHandler<T> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (DatabaseDialectUtils.isPostgresql()) {
PGobject jsonObject = new PGobject();
jsonObject.setType("json");
jsonObject.setValue(toJson(parameter));
ps.setObject(i, jsonObject);
} else {
ps.setString(i, toJson(parameter));
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
final String json = rs.getString(columnName);
return StringUtil.noText(json) ? null : parseJson(json);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
final String json = rs.getString(columnIndex);
return StringUtil.noText(json) ? null : parseJson(json);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
final String json = cs.getString(columnIndex);
return StringUtil.noText(json) ? null : parseJson(json);
}
protected abstract T parseJson(String json);
protected abstract String toJson(T object);
}
@@ -0,0 +1,73 @@
package com.cool.core.mybatis.handler;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.TypeReference;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
public class Fastjson2TypeHandler extends BaseJsonTypeHandler<Object> {
private final Class<?> propertyType;
private Class<?> genericType;
private Type type;
private boolean supportAutoType = false;
public Fastjson2TypeHandler(Class<?> propertyType) {
this.propertyType = propertyType;
this.supportAutoType = propertyType.isInterface() || Modifier.isAbstract(propertyType.getModifiers());
}
public Fastjson2TypeHandler(Class<?> propertyType, Class<?> genericType) {
this.propertyType = propertyType;
this.genericType = genericType;
this.type = TypeReference.collectionType((Class<? extends Collection>) propertyType, genericType);
Type actualTypeArgument = ((ParameterizedType) type).getActualTypeArguments()[0];
if (actualTypeArgument instanceof Class) {
this.supportAutoType = ((Class<?>) actualTypeArgument).isInterface()
|| Modifier.isAbstract(((Class<?>) actualTypeArgument).getModifiers());
}
}
@Override
protected Object parseJson(String json) {
if (genericType != null && Collection.class.isAssignableFrom(propertyType)) {
if (supportAutoType) {
return JSON.parseArray(json, Object.class, JSONReader.Feature.SupportAutoType);
} else {
return JSON.parseObject(json, type);
}
} else {
if (supportAutoType) {
return JSON.parseObject(json, Object.class, JSONReader.Feature.SupportAutoType);
} else {
return JSON.parseObject(json, propertyType);
}
}
}
@Override
protected String toJson(Object object) {
if (supportAutoType) {
return JSON.toJSONString(object
, JSONWriter.Feature.WriteMapNullValue
, JSONWriter.Feature.WriteNullListAsEmpty
, JSONWriter.Feature.WriteNullStringAsEmpty, JSONWriter.Feature.WriteClassName
);
} else {
return JSON.toJSONString(object
, JSONWriter.Feature.WriteMapNullValue
, JSONWriter.Feature.WriteNullListAsEmpty
, JSONWriter.Feature.WriteNullStringAsEmpty
);
}
}
}
@@ -0,0 +1,68 @@
package com.cool.core.mybatis.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mybatisflex.core.exception.FlexExceptions;
import java.io.IOException;
import java.util.Collection;
public class JacksonTypeHandler extends BaseJsonTypeHandler<Object> {
private static ObjectMapper objectMapper;
private final Class<?> propertyType;
private Class<?> genericType;
private JavaType javaType;
public JacksonTypeHandler(Class<?> propertyType) {
this.propertyType = propertyType;
}
public JacksonTypeHandler(Class<?> propertyType, Class<?> genericType) {
this.propertyType = propertyType;
this.genericType = genericType;
}
@Override
protected Object parseJson(String json) {
try {
if (genericType != null && Collection.class.isAssignableFrom(propertyType)) {
return getObjectMapper().readValue(json, getJavaType());
} else {
return getObjectMapper().readValue(json, propertyType);
}
} catch (IOException e) {
throw FlexExceptions.wrap(e, "Can not parseJson by JacksonTypeHandler: " + json);
}
}
@Override
protected String toJson(Object object) {
try {
return getObjectMapper().writeValueAsString(object);
} catch (JsonProcessingException e) {
throw FlexExceptions.wrap(e, "Can not convert object to Json by JacksonTypeHandler: " + object);
}
}
public JavaType getJavaType() {
if (javaType == null){
javaType = getObjectMapper().getTypeFactory().constructCollectionType((Class<? extends Collection>) propertyType, genericType);
}
return javaType;
}
public static ObjectMapper getObjectMapper() {
if (null == objectMapper) {
objectMapper = new ObjectMapper();
}
return objectMapper;
}
public static void setObjectMapper(ObjectMapper objectMapper) {
JacksonTypeHandler.objectMapper = objectMapper;
}
}
@@ -0,0 +1,66 @@
package com.cool.core.mybatis.pg;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import java.util.Map;
/**
* PostgreSQL Identity 序列同步服务
* 解决PostgreSQL 默认的序列机制,序列会自动递增,当手动插入指定id时需调用同步接口,否则id会重复。
*/
@Slf4j
@Service
public class PostgresSequenceSyncService {
private final JdbcTemplate jdbcTemplate;
public PostgresSequenceSyncService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void syncIdentitySequences() {
log.info("⏳ 开始同步 PostgreSQL Identity 序列...");
// 查询所有 identity 字段
String identityColumnQuery = """
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE is_identity = 'YES'
AND table_schema = 'public'
""";
List<Map<String, Object>> identityColumns = jdbcTemplate.queryForList(identityColumnQuery);
for (Map<String, Object> col : identityColumns) {
String schema = (String) col.get("table_schema");
String table = (String) col.get("table_name");
String column = (String) col.get("column_name");
String fullTable = schema + "." + table;
// 获取对应的序列名
String seqNameSql = "SELECT pg_get_serial_sequence(?, ?)";
String seqName = jdbcTemplate.queryForObject(seqNameSql, String.class, fullTable, column);
if (seqName == null) {
log.warn("⚠️ 无法获取序列:{}.{}", table, column);
continue;
}
// 获取当前最大 ID
Long maxId = jdbcTemplate.queryForObject(
String.format("SELECT COALESCE(MAX(%s), 0) FROM %s", column, fullTable),
Long.class
);
if (maxId != null && maxId > 0) { // 正确的:setval 有返回值,必须用 queryForObject
String setvalSql = "SELECT setval(?, ?)";
Long newVal = jdbcTemplate.queryForObject(setvalSql, Long.class, seqName, maxId);
log.info("✅ 同步序列 [{}] -> 当前最大 ID: {}", seqName, newVal);
}
}
log.info("✅ PostgreSQL Identity 序列同步完成。");
}
}
@@ -4,7 +4,10 @@ package com.cool.core.plugin.consts;
* 常量工具
*/
public interface PluginConsts {
/**
* 国际化插件
*/
String i18n = "i18n";
/**
* 上传文件hook
*/
@@ -0,0 +1,7 @@
package com.cool.core.plugin.event;
public enum PluginActionEnum {
INSTALL,
UNINSTALL,
UPDATE,
}
@@ -0,0 +1,26 @@
package com.cool.core.plugin.event;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import java.time.Clock;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
public class PluginEvent extends ApplicationEvent {
@Getter
private String key;
@Getter
private PluginActionEnum actionEnum;
@Getter
private PluginInfoEntity pluginInfoEntity;
public PluginEvent(Object source, String key, PluginActionEnum actionEnum, PluginInfoEntity data) {
super(source);
this.key = key;
this.actionEnum = actionEnum;
this.pluginInfoEntity = data;
}
public PluginEvent(Object source, Clock clock) {
super(source, clock);
}
}
@@ -0,0 +1,46 @@
package com.cool.core.plugin.event;
import static com.cool.core.plugin.consts.PluginConsts.i18n;
import cn.hutool.core.util.ObjUtil;
import com.cool.core.i18n.I18nGenerator;
import com.cool.core.util.I18nUtil;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class PluginEventListener implements ApplicationListener<PluginEvent> {
private final I18nGenerator i18nGenerator;
private final I18nUtil i18nUtil;
@Async
@Override
public void onApplicationEvent(PluginEvent event) {
if (ObjUtil.equals(event.getKey(), i18n)) {
// 国际化插件变更
PluginActionEnum actionEnum = event.getActionEnum();
PluginInfoEntity pluginInfoEntity = event.getPluginInfoEntity();
if (ObjUtil.equals(actionEnum, PluginActionEnum.INSTALL) && ObjUtil.equals(pluginInfoEntity.getStatus(), 1)) {
// 安装插件后,如果插件状态为启用,则生成国际化文件
i18nGenerator.run((Map<String, Object>) pluginInfoEntity.getConfig());
} else if (ObjUtil.equals(actionEnum, PluginActionEnum.UPDATE)) {
if (ObjUtil.equals(pluginInfoEntity.getStatus(), 1)) {
// 更新插件配置
i18nGenerator.run((Map<String, Object>) pluginInfoEntity.getConfig());
} else {
// 停用
I18nUtil.enable = false;
}
} else if (ObjUtil.equals(actionEnum, PluginActionEnum.UNINSTALL)) {
// 卸载国际化插件,则删除国际化文件
i18nUtil.clear();
}
}
}
}
@@ -0,0 +1,19 @@
package com.cool.core.plugin.event;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class PluginEventPublisher {
private final ApplicationEventPublisher applicationEventPublisher;
public void publish(String key, PluginActionEnum actionEnum, PluginInfoEntity data) {
PluginEvent event = new PluginEvent(this, key, actionEnum, data);
applicationEventPublisher.publishEvent(event);
}
}
@@ -11,6 +11,8 @@ import com.cool.core.config.PluginJson;
import com.cool.core.exception.CoolException;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.plugin.config.DynamicJarClassLoader;
import com.cool.core.plugin.event.PluginActionEnum;
import com.cool.core.plugin.event.PluginEventPublisher;
import com.cool.core.util.CoolPluginInvokers;
import com.cool.core.util.MapExtUtil;
import com.cool.core.util.PathUtils;
@@ -24,6 +26,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.exceptions.PersistenceException;
@@ -43,14 +46,15 @@ public class CoolPluginService {
final private PluginInfoService pluginInfoService;
final private PluginEventPublisher pluginEventPublisher;
@Value("${cool.plugin.path}")
private String pluginPath;
public void init() {
List<PluginInfoEntity> list = pluginInfoService
.list(QueryWrapper
.create().select(PluginInfoEntity::getId, PluginInfoEntity::getPluginJson,
PluginInfoEntity::getKey, PluginInfoEntity::getName)
.create()
.eq(PluginInfoEntity::getStatus, 1));
if (ObjUtil.isEmpty(list)) {
log.info("没有可初始化的插件");
@@ -77,6 +81,7 @@ public class CoolPluginService {
dynamicJarLoaderService.install(pluginJson.getJarPath(), true);
// 设置配置
CoolPluginInvokers.setPluginJson(entity.getKey(), entity);
pluginEventPublisher.publish(entity.getKey(), PluginActionEnum.INSTALL, entity);
} catch (Exception e) {
log.error("初始化{}插件失败", entity.getName(), e);
} finally {
@@ -100,9 +105,10 @@ public class CoolPluginService {
PluginJson pluginJson = dynamicJarLoaderService.install(jarFilePath, force);
key = pluginJson.getKey();
// 保存插件信息入库
savePluginInfo(pluginJson, jarFilePath, jarFile, force);
PluginInfoEntity pluginInfoEntity = savePluginInfo(pluginJson, jarFilePath, jarFile, force);
// 把 ApplicationContext 对象传递打插件类中,使其在插件中也能正常使用spring bean对象
CoolPluginInvokers.setApplicationContext(pluginJson.getKey());
pluginEventPublisher.publish(pluginJson.getKey(), PluginActionEnum.INSTALL, pluginInfoEntity);
} catch (PersistenceException persistenceException) {
extractedAfterErr(jarFile, key);
if (persistenceException.getMessage().contains("Duplicate entry")) {
@@ -165,13 +171,14 @@ public class CoolPluginService {
boolean flag = pluginInfoEntity.removeById();
if (flag) {
FileUtil.del(pluginInfoEntity.getPluginJson().getJarPath());
pluginEventPublisher.publish(pluginInfoEntity.getKey(), PluginActionEnum.UNINSTALL, null);
}
}
/**
* 保存插件信息
*/
private void savePluginInfo(PluginJson pluginJson, String jarFilePath ,
private PluginInfoEntity savePluginInfo(PluginJson pluginJson, String jarFilePath ,
File jarFile,
boolean force) {
CoolPreconditions.checkEmpty(pluginJson, "插件安装失败");
@@ -186,9 +193,11 @@ public class CoolPluginService {
closeSameNamePlugin(pluginJson);
// 覆盖插件
coverPlugin(pluginJson, pluginInfo);
return;
return pluginInfo;
}
pluginInfo.setStatus(1);
pluginInfo.save();
return pluginInfo;
}
/**
@@ -201,7 +210,7 @@ public class CoolPluginService {
String oldJarPath = one.getPluginJson().getJarPath();
// 重新加载配置不更新
pluginInfo.setConfig(one.getConfig());
pluginInfo.getPluginJson().setConfig(one.getConfig());
pluginInfo.getPluginJson().setConfig((Map<String, Object>) one.getConfig());
// 设置插件配置
CoolPluginInvokers.setPluginJson(pluginInfo.getKey(), pluginInfo);
CopyOptions options = CopyOptions.create().setIgnoreNullValue(true);
@@ -274,10 +283,11 @@ public class CoolPluginService {
entity.getId());
// 调用插件更新配置标识
boolean invokePluginConfig = false;
if (!MapExtUtil.compareMaps(entity.getConfig(), dbPluginInfoEntity.getConfig())) {
if (!MapExtUtil.compareMaps((Map<String, Object>) entity.getConfig(),
(Map<String, Object>) dbPluginInfoEntity.getConfig())) {
// 不一致,说明更新了配置
entity.setPluginJson(dbPluginInfoEntity.getPluginJson());
entity.getPluginJson().setConfig(entity.getConfig());
entity.getPluginJson().setConfig((Map<String, Object>) entity.getConfig());
// 更新了配置, 且插件是开启状态
invokePluginConfig = ObjUtil.equals(dbPluginInfoEntity.getStatus(), 1);
}
@@ -290,6 +300,8 @@ public class CoolPluginService {
CoolPluginInvokers.setPluginJson(dbPluginInfoEntity.getKey(), entity);
}
pluginInfoService.update(entity);
pluginEventPublisher.publish(dbPluginInfoEntity.getKey(), PluginActionEnum.UPDATE, pluginInfoService.getPluginInfoEntityById(
entity.getId()));
}
/**
@@ -10,6 +10,7 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.cool.core.enums.QueryModeEnum;
import com.cool.core.util.ConvertUtil;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.query.QueryColumn;
import com.mybatisflex.core.query.QueryCondition;
@@ -37,12 +38,18 @@ public class CrudOption<T> {
private QueryModeEnum queryModeEnum;
private Transform<Object> transform;
private TransformValue<Object> transformValue;
public interface Transform<B> {
private TransformParam<JSONObject> transformParam;
public interface TransformValue<B> {
void apply(B obj);
}
public interface TransformParam<JSONObject> {
void apply(JSONObject obj);
}
/**
* queryModeEnum 为 CUSTOM,可设置 默认为Map
*/
@@ -97,7 +104,7 @@ public class CrudOption<T> {
public CrudOption<T> queryModeEnum(QueryModeEnum queryModeEnum) {
this.queryModeEnum = queryModeEnum;
if (ObjUtil.equal(queryModeEnum, QueryModeEnum.CUSTOM)
&& ObjUtil.isEmpty(asType)) {
&& ObjUtil.isEmpty(asType)) {
asType = Map.class;
}
return this;
@@ -112,10 +119,27 @@ public class CrudOption<T> {
}
/**
* 转换参数,组装数据
* 转换值,组装数据
*/
public CrudOption<T> transform(Transform<Object> transform) {
this.transform = transform;
public CrudOption<T> transformValue(TransformValue<Object> transformValue) {
this.transformValue = transformValue;
return this;
}
/**
* 请使用 transformValue
*/
@Deprecated
public CrudOption<T> transform(TransformValue<Object> transformValue) {
this.transformValue = transformValue;
return this;
}
/**
* 转换入参
*/
public CrudOption<T> transformParam(TransformParam<JSONObject> transformParam) {
this.transformParam = transformParam;
return this;
}
@@ -127,32 +151,46 @@ public class CrudOption<T> {
private QueryWrapper build(QueryWrapper queryWrapper, Class<T> entityClass) {
if (ObjectUtil.isNotEmpty(fieldEq)) {
Arrays.stream(fieldEq).toList().forEach(filed -> {
Object obj = requestParams.get(StrUtil.toCamelCase(filed.getName()));
String filedName = StrUtil.toCamelCase(filed.getName());
Object obj = requestParams.get(filedName);
if (ObjUtil.isEmpty(obj)) {
return;
}
if (obj instanceof JSONArray) {
// 集合
queryWrapper.and(filed.in(((JSONArray)obj).toArray()));
queryWrapper.and(filed.in(ConvertUtil.covertListByClass(filedName, (JSONArray)obj, entityClass).toArray()));
} else {
// 对象
queryWrapper.and(filed.eq(obj));
queryWrapper.and(filed.eq(ConvertUtil.convertByClass(filedName, obj, entityClass)));
}
});
}
Object keyWord = requestParams.get("keyWord");
if (ObjectUtil.isNotEmpty(this.keyWordLikeFields) && ObjectUtil.isNotEmpty(keyWord)) {
// 初始化一个空的 QueryCondition
QueryCondition orCondition = null;
for (QueryColumn queryColumn : keyWordLikeFields) {
QueryCondition condition = queryColumn.like(keyWord);
if (orCondition == null) {
orCondition = condition;
} else {
orCondition = orCondition.or(condition);
if (ObjectUtil.isNotEmpty(this.keyWordLikeFields)) {
Object keyWord = requestParams.get("keyWord");
if (ObjectUtil.isEmpty(keyWord)) {
// // keyWord值为空,遍历keyWordLikeFields字段,根据queryColumn字段名构建查询条件
for (QueryColumn queryColumn : keyWordLikeFields) {
String fieldName = queryColumn.getName();
String paramName = StrUtil.toCamelCase(fieldName);
String paramValue = requestParams.getStr(paramName);
if (ObjectUtil.isNotEmpty(paramValue)) {
queryWrapper.and(queryColumn.like(paramValue));
}
}
} else {
// keyWord值非空,使用keyWord构建
// 初始化一个空的 QueryCondition
QueryCondition orCondition = null;
for (QueryColumn queryColumn : keyWordLikeFields) {
QueryCondition condition = queryColumn.like(keyWord);
if (orCondition == null) {
orCondition = condition;
} else {
orCondition = orCondition.or(condition);
}
}
queryWrapper.and(orCondition);
}
queryWrapper.and(orCondition);
}
if (ObjectUtil.isNotEmpty(select)) {
queryWrapper.select(select);
@@ -176,12 +214,12 @@ public class CrudOption<T> {
tableAlias = queryTable.getName() + ".";
}
String order = requestParams.getStr("order",
tableAnnotation.camelToUnderline() ? "create_time" : "createTime");
tableAnnotation.camelToUnderline() ? "create_time" : "createTime");
String sort = requestParams.getStr("sort", "desc");
if (StrUtil.isNotEmpty(order) && StrUtil.isNotEmpty(sort)) {
queryWrapper.orderBy(
tableAlias + (tableAnnotation.camelToUnderline() ? StrUtil.toUnderlineCase(order) : order),
sort.equals("asc"));
tableAlias + (tableAnnotation.camelToUnderline() ? StrUtil.toUnderlineCase(order) : order),
sort.equals("asc"));
}
}
}
}
@@ -1,11 +1,33 @@
package com.cool.core.request;
import java.util.List;
import java.util.Map;
import com.mybatisflex.core.paginate.Page;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class PageResult {
private List<?> list;
private Map<String, Object> pagination;
@Schema( title = "分页数据模型" )
public class PageResult<T> {
@Schema( title = "分页数据" )
private List<T> list;
private Pagination pagination = new Pagination();
@Data
public static class Pagination {
@Schema( title = "页码" )
private Long page;
@Schema( title = "本页数量" )
private Long size;
@Schema( title = "总页数" )
private Long total;
}
static public <B> PageResult<B> of(Page<B> page ){
PageResult<B> result = new PageResult<B>();
result.setList(page.getRecords());
result.pagination.setPage( page.getPageNumber() );
result.pagination.setSize( page.getPageSize() );
result.pagination.setTotal( page.getTotalRow() );
return result;
}
}
+36 -11
View File
@@ -1,16 +1,36 @@
package com.cool.core.request;
import java.util.HashMap;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 返回信息
*/
public class R extends HashMap<String, Object> {
@Schema(title = "响应数据结构")
@Data
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(title = "编码:1000表示成功,其他值表示失败")
private int code = 1000;
@Schema(title = "消息内容")
private String message = "success";
@Schema(title = "响应数据")
private T data;
public R() {
put("code", 1000);
put("message", "success");
}
public R( int code, String message, T data ) {
this.code = code;
this.message = message;
this.data = data;
}
public static R error() {
@@ -23,14 +43,14 @@ public class R extends HashMap<String, Object> {
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("message", msg);
r.code = code;
r.message = msg;
return r;
}
public static R okMsg(String msg) {
R r = new R();
r.put("message", msg);
r.message = msg;
return r;
}
@@ -38,12 +58,17 @@ public class R extends HashMap<String, Object> {
return new R();
}
public static R ok(Object data) {
return new R().put("data", data);
public static <B> R<B> ok(B data) {
return new R<B>(1000 , "success", data);
}
public R put(String key, Object value) {
super.put(key, value);
public R<T> put(String key, Object value) {
switch (key) {
case "code" -> this.code = (int) value;
case "message" -> this.message = (String) value;
case "data" -> this.data = (T) value;
}
return this;
}
}
@@ -1,5 +1,6 @@
package com.cool.core.request;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
@@ -31,12 +32,17 @@ public class RequestParamsFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
throws IOException, ServletException {
// 防止流读取一次后就没有了, 所以需要将流继续写出去
HttpServletRequest request = (HttpServletRequest) servletRequest;
JSONObject requestParams = new JSONObject();
String language = request.getHeader("language");
String coolEid = request.getHeader("cool-admin-eid");
Long tenantId = null;
if (StrUtil.isNotEmpty(request.getContentType()) && request.getContentType().contains("multipart/form-data")) {
servletRequest.setAttribute("requestParams", requestParams);
servletRequest.setAttribute("cool-language", language);
servletRequest.setAttribute("tenantId", tenantId);
filterChain.doFilter(servletRequest, servletResponse);
} else {
BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
@@ -45,38 +51,50 @@ public class RequestParamsFilter implements Filter {
body)) {
requestParams = JSONUtil.parseObj(body);
}
// 登录状态,设置用户id
setUserId(requestParams);
requestParams.set("body", body);
requestParams.putAll(getAllRequestParam(request));
Object jwtObj = request.getAttribute("tokenInfo");
if (jwtObj != null) {
requestParams.set("tokenInfo", ((JWT) jwtObj).getPayload().getClaimsJson());
}
requestWrapper.setAttribute("requestParams", requestParams);
// 登录状态,设置用户id
Long currTenantId = setUserId(requestParams);
if (ObjUtil.isNotNull(currTenantId)) {
tenantId = currTenantId;
}
requestWrapper.setAttribute("cool-language", language);
request.setAttribute("tenantId", tenantId);
requestParams.set("body", body);
requestParams.putAll(getAllRequestParam(request));
requestWrapper.setAttribute("requestParams", requestParams);
filterChain.doFilter(requestWrapper, servletResponse);
}
}
private void setUserId(JSONObject requestParams) {
private Long setUserId(JSONObject requestParams) {
UserTypeEnum userTypeEnum = CoolSecurityUtil.getCurrentUserType();
switch (userTypeEnum) {
// 只有登录了,才有用户类型, 不然为 UNKNOWN 状态
case ADMIN -> {
// 管理后台由于之前已经有逻辑再了,怕会影响到,如果自己有传了值不覆盖
Object o = requestParams.get("userId");
if (ObjUtil.isNotEmpty(o)) {
return;
if (ObjUtil.isNull(o)) {
requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
}
requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
}
// app端,userId 为当前登录的用户id
case APP -> requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
case APP -> {
requestParams.set("userId", CoolSecurityUtil.getCurrentUserId());
// App端:从tokenInfo中读取orgId作为tenantId,实现组织级数据隔离
Object jwtObj = requestParams.get("tokenInfo");
if (jwtObj instanceof Map) {
Object orgId = ((Map<?, ?>) jwtObj).get("orgId");
if (ObjUtil.isNotNull(orgId)) {
requestParams.set("tenantId", Convert.toLong(orgId));
}
}
}
}
return CoolSecurityUtil.getTenantId(requestParams);
}
/**
@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.enums.Apis;
import com.cool.core.util.ConvertUtil;
import java.lang.reflect.Method;
import java.util.List;
@@ -43,7 +44,7 @@ public class AutoPrefixUrlMapping extends RequestMappingHandlerMapping {
* @return 是否需要构建路由
*/
private boolean checkApis(CoolRestController[] annotations, RequestMappingInfo info) {
String[] apis = new String[] { "add", "delete", "update", "page", "list", "info" };
String[] apis = Apis.ALL_API;
if (info.getPathPatternsCondition() == null) {
return true;
}
@@ -0,0 +1,93 @@
package com.cool.core.security;
import cn.hutool.core.util.StrUtil;
import com.cool.core.exception.CoolException;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.org.entity.AppOrgEntity;
import com.cool.modules.org.entity.AppOrgModuleEntity;
import com.cool.modules.org.entity.AppOrgUserEntity;
import com.cool.modules.org.service.AppOrgModuleService;
import com.cool.modules.org.service.AppOrgUserService;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@Order(3)
@RequiredArgsConstructor
public class AppOrgAuthFilter extends OncePerRequestFilter {
private final AppOrgUserService appOrgUserService;
private final AppOrgModuleService appOrgModuleService;
private static final Map<String, String> MODULE_PATH_MAP = Map.of(
"/app/fruit/", "fruit",
"/app/order/", "order",
"/app/book/", "book",
"/app/ticket/", "ticket",
"/app/product/", "product",
"/app/agent/", "agent"
);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String uri = request.getRequestURI();
if (!uri.startsWith("/app/") || uri.startsWith("/app/org/") || uri.startsWith("/app/user/") || uri.startsWith("/app/base/") || uri.startsWith("/app/dict/")) {
chain.doFilter(request, response);
return;
}
if (com.cool.core.enums.UserTypeEnum.APP != CoolSecurityUtil.getCurrentUserType()) {
chain.doFilter(request, response);
return;
}
String moduleKey = resolveModuleKey(uri);
if (moduleKey == null) {
chain.doFilter(request, response);
return;
}
Long userId = CoolSecurityUtil.getCurrentUserId();
String orgIdStr = request.getHeader("X-Org-Id");
if (StrUtil.isBlank(orgIdStr)) {
throw new CoolException("缺少组织信息", 403);
}
Long orgId;
try {
orgId = Long.parseLong(orgIdStr);
} catch (NumberFormatException e) {
throw new CoolException("无效的组织ID", 403);
}
if (!appOrgUserService.isOrgMember(userId, orgId)) {
throw new CoolException("未加入该组织", 403);
}
if (!appOrgModuleService.isModuleAuthorized(orgId, moduleKey)) {
throw new CoolException("该组织未授权此模块", 403);
}
chain.doFilter(request, response);
}
private String resolveModuleKey(String uri) {
for (Map.Entry<String, String> entry : MODULE_PATH_MAP.entrySet()) {
if (uri.startsWith(entry.getKey())) {
return entry.getValue();
}
}
return null;
}
}
@@ -15,12 +15,14 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -32,6 +34,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
import org.springframework.web.util.pattern.PathPattern;
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
@Slf4j
@RequiredArgsConstructor
@@ -40,6 +43,7 @@ public class JwtSecurityConfig {
// 用户详情
final private UserDetailsService userDetailsService;
final private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
final private AppOrgAuthFilter appOrgAuthFilter;
// 401
final private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
// 403
@@ -69,6 +73,8 @@ public class JwtSecurityConfig {
.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationTokenFilter,
UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(appOrgAuthFilter,
JwtAuthenticationTokenFilter.class)
.exceptionHandling(config -> {
config.authenticationEntryPoint(entryPointUnauthorizedHandler);
config.accessDeniedHandler(restAccessDeniedHandler);
@@ -123,15 +129,19 @@ public class JwtSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder();
@Override
public String encode(CharSequence rawPassword) {
return DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes());
return bcrypt.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(
DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes()));
if (encodedPassword.startsWith("$2a$") || encodedPassword.startsWith("$2b$") || encodedPassword.startsWith("$2y$")) {
return bcrypt.matches(rawPassword, encodedPassword);
}
return encodedPassword.equals(DigestUtils.md5DigestAsHex(((String) rawPassword).getBytes()));
}
};
}
@@ -1,5 +1,8 @@
package com.cool.core.security;
import cn.hutool.core.util.ObjectUtil;
import com.cool.core.enums.UserTypeEnum;
import com.cool.core.security.jwt.JwtUser;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -27,20 +30,22 @@ public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
if (configAttributes == null) {
return;
}
List<String> urls = ignoredUrlsProperties.getAdminAuthUrls();
String url = ((FilterInvocation) o).getRequestUrl().split("[?]")[0];
if (urls.contains(url)) {
return;
}
if (configAttributes == null) {
if (isSuperAdmin(authentication)) {
return;
}
throw new AccessDeniedException("抱歉,您没有访问权限");
}
Iterator<ConfigAttribute> iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute c = iterator.next();
String needPerm = c.getAttribute();
for (GrantedAuthority ga : authentication.getAuthorities()) {
// 匹配用户拥有的ga 和 系统中的needPerm
if (needPerm.trim().equals(ga.getAuthority())) {
return;
}
@@ -49,6 +54,18 @@ public class MyAccessDecisionManager implements AccessDecisionManager {
throw new AccessDeniedException("抱歉,您没有访问权限");
}
private boolean isSuperAdmin(Authentication authentication) {
Object principal = authentication.getPrincipal();
if (principal instanceof JwtUser) {
JwtUser jwtUser = (JwtUser) principal;
if (ObjectUtil.equal(jwtUser.getUserTypeEnum(), UserTypeEnum.APP)) {
return false;
}
return "admin".equals(jwtUser.getUsername());
}
return false;
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
@@ -0,0 +1,14 @@
package com.cool.core.tenant;
import com.cool.core.util.TenantUtil;
import com.mybatisflex.core.tenant.TenantFactory;
public class CoolTenantFactory implements TenantFactory {
public Object[] getTenantIds(){
Long tenantId = TenantUtil.getTenantId();
if (tenantId == null) {
return null;
}
return new Object[]{tenantId};
}
}
@@ -0,0 +1,27 @@
package com.cool.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import java.io.Serializable;
public class AutoTypeConverter {
/**
* 将字符串自动转换为数字或保留为字符串
*
* @param input 输入字符串
* @return Integer / Long / String
*/
public static Serializable autoConvert(Object input) {
if (input == null) {
return null;
}
if (NumberUtil.isInteger(input.toString())) {
return Convert.convert(Integer.class, input);
} else if (NumberUtil.isLong(input.toString())) {
return Convert.convert(Long.class, input);
} else {
return (Serializable) input;
}
}
}
@@ -1,16 +1,13 @@
package com.cool.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.web.multipart.MultipartFile;
@@ -203,4 +200,58 @@ public class ConvertUtil {
private static String[] splitCamelCase(String input) {
return input.split("(?<=.)(?=[A-Z])");
}
/**
* 将给定的字段值转换为可序列化的形式
* 此方法旨在将一个对象的特定字段值转换为其相应的可序列化类型
* 它在序列化和反序列化过程中特别有用,确保字段值可以被正确处理
*
* @param fieldName 字段名称,用于查找字段类型
* @param fieldValue 待转换的字段值
* @param clazz 包含该字段的类
* @return 转换后的可序列化字段值,如果无法确定字段类型,则返回原始值
*/
public static Object convertByClass(String fieldName, Object fieldValue, Class<?> clazz) {
// 检查输入参数是否为空,如果字段名或字段值为空,则直接返回字段值
if (fieldName == null || fieldValue == null) {
return fieldValue;
}
// 获取字段类型
Class<?> fieldType = getFieldType(clazz, fieldName);
// 如果字段类型为空,则直接返回字段值
if (fieldType == null) {
return fieldValue;
}
// 使用Convert类的convert方法将字段值转换为字段类型
return Convert.convert(fieldType, fieldValue);
}
public static List<Object> covertListByClass(String fieldName, List<Object> fieldValue, Class<?> clazz) {
// 检查输入参数是否为空,如果字段名或字段值为空,则直接返回字段值
if (fieldName == null || fieldValue == null) {
return fieldValue;
}
// 获取字段类型
Class<?> fieldType = getFieldType(clazz, fieldName);
// 如果字段类型为空,则直接返回字段值
if (fieldType == null) {
return fieldValue;
}
return Collections.singletonList(Convert.toList(fieldType, fieldValue));
}
/**
* 获取指定类中指定字段的类型
*
* @param clazz 目标类
* @param fieldName 字段名称
* @return 字段的类型 Class,如果字段不存在则返回 null
*/
public static Class<?> getFieldType(Class<?> clazz, String fieldName) {
Field field = ReflectUtil.getField(clazz, fieldName);
return field != null ? field.getType() : null;
}
}
@@ -7,10 +7,16 @@ import com.cool.core.enums.UserTypeEnum;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.security.jwt.JwtUser;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import com.cool.modules.base.entity.sys.BaseSysUserRoleEntity;
import com.cool.modules.base.mapper.sys.BaseSysUserMapper;
import com.cool.modules.base.mapper.sys.BaseSysUserRoleMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.List;
/**
* Security 工具类
*/
@@ -26,6 +32,27 @@ public class CoolSecurityUtil {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
/**
* 是否为超级管理员
*/
public static boolean isSuperAdmin() {
return isSuperAdmin(getAdminUsername());
}
public static boolean isSuperAdmin(String username) {
BaseSysUserEntity userEntity = SpringUtil.getBean(BaseSysUserMapper.class)
.selectOneByQuery(com.mybatisflex.core.query.QueryWrapper.create()
.eq(BaseSysUserEntity::getUsername, username));
if (userEntity == null) {
return false;
}
BaseSysUserRoleMapper userRoleMapper = SpringUtil.getBean(BaseSysUserRoleMapper.class);
List<BaseSysUserRoleEntity> userRoles = userRoleMapper
.selectListByQuery(com.mybatisflex.core.query.QueryWrapper.create()
.eq(BaseSysUserRoleEntity::getUserId, userEntity.getId())
.eq(BaseSysUserRoleEntity::getRoleId, 1L));
return !userRoles.isEmpty();
}
/**
* 获得jwt中的信息
*
@@ -42,6 +69,14 @@ public class CoolSecurityUtil {
return tokenInfo;
}
public static Long getTenantId(JSONObject requestParams) {
JSONObject tokenInfo = requestParams.getJSONObject("tokenInfo");
if (tokenInfo != null) {
return tokenInfo.getLong("tenantId");
}
return null;
}
/**
* 后台账号退出登录
*
@@ -50,7 +85,8 @@ public class CoolSecurityUtil {
*/
public static void adminLogout(Long adminUserId, String username) {
coolCache.del("admin:department:" + adminUserId, "admin:passwordVersion:" + adminUserId,
"admin:userInfo:" + adminUserId, "admin:userDetails:" + username);
"admin:userInfo:" + adminUserId, "admin:userDetails:" + username,
"admin:roleIds:" + adminUserId, "admin:perms:" + adminUserId);
}
/**
@@ -12,16 +12,25 @@ import javax.sql.DataSource;
public class DatabaseDialectUtils {
private static String dialect;
public static String getDatabaseDialect() {
public static String getDatabaseDialect(DataSource dataSource) {
if (dialect == null) {
dialect = determineDatabaseType();
dialect = determineDatabaseType(dataSource);
}
return dialect;
}
private static String determineDatabaseType() {
// 从 DataSource 获取连接
public static boolean isPostgresql() {
DataSource dataSource = SpringContextUtils.getBean(DataSource.class);
return DatabaseDialect.PostgreSQL.equals(getDatabaseDialect(dataSource));
}
public static boolean isPostgresql(DataSource dataSource) {
return DatabaseDialect.PostgreSQL.equals(getDatabaseDialect(dataSource));
}
private static String determineDatabaseType(DataSource dataSource) {
// 从 DataSource 获取连接
try (Connection connection = dataSource.getConnection()) {
// 获取元数据
DatabaseMetaData metaData = connection.getMetaData();
@@ -0,0 +1,125 @@
package com.cool.core.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
@Slf4j
@Component
public class I18nUtil {
public static final String MSG_PREFIX = "msg_";
public static final String MENU_PREFIX = "menu_";
public static final String DICT_INFO_PREFIX = "dictInfo_";
public static final String DICT_TYPE_PREFIX = "dictType_";
public static boolean enable = false;
public static String path;
public static String getLanguage() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return null;
}
return (String) attributes.getAttribute("cool-language", RequestAttributes.SCOPE_REQUEST);
}
private static final Map<String, JSONObject> data = new ConcurrentHashMap<>();
private void load(String key, File file) {
try {
String content = FileUtil.readUtf8String(file);
data.put(key, JSONUtil.parseObj(content));
} catch (Exception e) {
log.error("读取国际化文件失败", e);
}
}
public boolean exist(String name) {
// 获取该目录下所有的 .json 文件
List<File> jsonFiles = FileUtil.loopFiles(getPath(), file ->
file.isFile() && file.getName().endsWith(".json")
);
AtomicReference<Boolean> flag = new AtomicReference<>(false);
jsonFiles.forEach(file -> {
String parentName = file.getParentFile().getName();
String key = parentName + "_" + file.getName().replace(".json", "");
if (key.equals(name)) {
flag.set(true);
// 加载
load(key, file);
}
});
return flag.get();
}
public static String getI18nMenu(String name) {
return getI18n(name, MENU_PREFIX);
}
public static String getI18nMsg(String name) {
return getI18n(name, MSG_PREFIX);
}
public static String getI18nDictInfo(String name) {
return getI18n(name, DICT_INFO_PREFIX);
}
public static String getI18nDictType(String name) {
return getI18n(name, DICT_TYPE_PREFIX);
}
private static String getI18n(String name, String prefix) {
if (!enable) {
return name;
}
String language = I18nUtil.getLanguage();
if (language == null) {
return name;
}
JSONObject jsonObject = data.get(prefix + language);
if (jsonObject == null) {
return name;
}
String str = jsonObject.getStr(name);
if (str == null) {
return name;
}
return str;
}
public void update(String key, JSONObject object) {
data.put(key, object);
String[] split = key.split("_");
String absolutePath = getPath();
File file = FileUtil.file(absolutePath, split[0], split[1] + ".json");
// 确保父目录存在
FileUtil.mkParentDirs(file);
// 写入内容
FileUtil.writeUtf8String(JSONUtil.toJsonStr(object), file);
}
private String getPath() {
String absolutePath = path;
if (!PathUtils.isAbsolutePath(absolutePath)) {
absolutePath = PathUtils.getUserDir() + File.separator + absolutePath;
}
return absolutePath;
}
public void clear() {
data.clear();
List<File> jsonFiles = FileUtil.loopFiles(getPath(), file ->
file.isFile() && file.getName().endsWith(".json")
);
jsonFiles.forEach(File::delete);
enable = false;
}
}
@@ -0,0 +1,14 @@
package com.cool.core.util;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
public class TenantUtil {
public static Long getTenantId() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return null;
}
return (Long) attributes.getAttribute("tenantId", RequestAttributes.SCOPE_REQUEST);
}
}
@@ -0,0 +1,47 @@
package com.cool.modules.agent.controller.admin;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.request.R;
import com.cool.modules.agent.entity.AgentInfoEntity;
import com.cool.modules.agent.service.AgentInfoService;
import com.cool.modules.agent.service.SayssService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
@RequiredArgsConstructor
@Tag(name = "代理管理", description = "代理信息管理")
@CoolRestController(api = {"add", "delete", "update", "page", "info"})
public class AdminAgentInfoController extends BaseController<AgentInfoService, AgentInfoEntity> {
private final SayssService sayssService;
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
setPageOption(createOp());
}
@Operation(summary = "登录SaySS")
@PostMapping("/loginSayss")
public R loginSayss(@RequestAttribute JSONObject requestParams) {
Long id = requestParams.getLong("id");
String authData = sayssService.loginAndSave(id);
if (authData == null) {
return R.error("登录SaySS失败,请检查用户名密码");
}
return R.ok();
}
@Operation(summary = "验证SaySS Token")
@PostMapping("/checkToken")
public R checkToken(@RequestAttribute JSONObject requestParams) {
Long id = requestParams.getLong("id");
boolean valid = sayssService.checkTokenValid(id);
return R.ok(new JSONObject().set("valid", valid));
}
}
@@ -0,0 +1,71 @@
package com.cool.modules.agent.controller.admin;
import cn.hutool.core.convert.Convert;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.request.R;
import com.cool.modules.agent.entity.AgentInfoEntity;
import com.cool.modules.agent.entity.AgentWhitelistEntity;
import com.cool.modules.agent.service.AgentInfoService;
import com.cool.modules.agent.service.AgentWhitelistService;
import com.cool.modules.agent.service.SayssService;
import com.cool.modules.user.entity.UserInfoEntity;
import com.cool.modules.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestAttribute;
@RequiredArgsConstructor
@Tag(name = "白名单管理", description = "代理白名单管理")
@CoolRestController(api = {"delete", "page"})
public class AdminAgentWhitelistController extends BaseController<AgentWhitelistService, AgentWhitelistEntity> {
private final AgentInfoService agentInfoService;
private final UserInfoService userInfoService;
private final SayssService sayssService;
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
setPageOption(createOp().transformValue(o -> {
AgentWhitelistEntity entity = (AgentWhitelistEntity) o;
AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(entity.getAgentInfoId());
if (agent != null) {
entity.setAgentName(agent.getName());
}
if (entity.getAddedByUserId() != null) {
UserInfoEntity user = (UserInfoEntity) userInfoService.info(entity.getAddedByUserId());
if (user != null) {
entity.setAddedByName(user.getNickName());
}
}
}));
}
@Operation(summary = "删除", description = "先从SaySS删除白名单,成功后再从本地删除")
@PostMapping("/delete")
protected R delete(HttpServletRequest request, @RequestBody Map<String, Object> params,
@RequestAttribute() JSONObject requestParams) {
Long[] ids = Convert.toLongArray(getIds(params));
for (Long id : ids) {
AgentWhitelistEntity entity = service.getById(id);
if (entity == null) continue;
boolean sayssDeleted;
if (entity.getSayssTrustedIpId() != null) {
sayssDeleted = sayssService.deleteTrustedIp(entity.getAgentInfoId(), entity.getSayssTrustedIpId());
} else {
sayssDeleted = sayssService.deleteTrustedIpByIp(entity.getAgentInfoId(), entity.getIpAddress());
}
if (!sayssDeleted) {
return R.error("从SaySS删除白名单失败,IP: " + entity.getIpAddress() + ",请检查代理账号是否正确");
}
}
service.delete(requestParams, ids);
return R.ok();
}
}
@@ -0,0 +1,117 @@
package com.cool.modules.agent.controller.app;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.request.R;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.agent.entity.AgentInfoEntity;
import com.cool.modules.agent.entity.AgentWhitelistEntity;
import com.cool.modules.agent.service.AgentInfoService;
import com.cool.modules.agent.service.AgentWhitelistService;
import com.cool.modules.agent.service.SayssService;
import com.mybatisflex.core.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import static com.cool.modules.agent.entity.table.AgentInfoEntityTableDef.AGENT_INFO_ENTITY;
import static com.cool.modules.agent.entity.table.AgentWhitelistEntityTableDef.AGENT_WHITELIST_ENTITY;
@Slf4j
@RequiredArgsConstructor
@Tag(name = "代理", description = "App端代理")
@CoolRestController(api = {"list"})
public class AppAgentInfoController {
private final AgentInfoService agentInfoService;
private final AgentWhitelistService agentWhitelistService;
private final SayssService sayssService;
@Operation(summary = "代理列表")
@GetMapping("/list")
public R list(@RequestAttribute JSONObject requestParams) {
return R.ok(agentInfoService.list(requestParams,
QueryWrapper.create().where(AGENT_INFO_ENTITY.STATUS.eq(1))));
}
@Operation(summary = "代理详情")
@GetMapping("/detail")
public R detail(@RequestAttribute JSONObject requestParams) {
Long id = requestParams.getLong("id");
AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(id);
if (agent == null || agent.getStatus() == null || agent.getStatus() != 1) {
return R.error("代理已停用");
}
return R.ok(agent);
}
@Operation(summary = "获取当前IP")
@GetMapping("/currentIp")
public R currentIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
JSONObject result = new JSONObject();
result.set("ip", ip);
return R.ok(result);
}
@Operation(summary = "添加白名单IP")
@PostMapping("/addWhitelist")
public R addWhitelist(@RequestAttribute JSONObject requestParams, HttpServletRequest request) {
Long agentInfoId = requestParams.getLong("agentInfoId");
String ipAddress = requestParams.getStr("ipAddress");
Long sayssTrustedIpId = sayssService.addTrustedIp(agentInfoId, ipAddress);
if (sayssTrustedIpId == null) {
log.warn("SaySS添加白名单失败,agentInfoId={}, ip={}", agentInfoId, ipAddress);
return R.error("添加白名单到SaySS失败,请检查代理账号是否正确");
}
Long addedByUserId = CoolSecurityUtil.getCurrentUserId();
AgentWhitelistEntity entity = new AgentWhitelistEntity();
entity.setAgentInfoId(agentInfoId);
entity.setIpAddress(ipAddress);
entity.setAddedByUserId(addedByUserId);
entity.setSayssTrustedIpId(sayssTrustedIpId);
agentWhitelistService.save(entity);
return R.ok(entity);
}
@Operation(summary = "获取白名单列表")
@GetMapping("/whitelist")
public R whitelist(@RequestAttribute JSONObject requestParams) {
Long agentInfoId = requestParams.getLong("agentInfoId");
QueryWrapper qw = QueryWrapper.create()
.where(AGENT_WHITELIST_ENTITY.AGENT_INFO_ID.eq(agentInfoId));
return R.ok(agentWhitelistService.list(requestParams, qw));
}
@Operation(summary = "检查IP是否在白名单")
@GetMapping("/checkIpInWhitelist")
public R checkIpInWhitelist(@RequestAttribute JSONObject requestParams) {
Long agentInfoId = requestParams.getLong("agentInfoId");
String ip = requestParams.getStr("ip");
boolean inWhitelist = sayssService.isIpInTrustedIps(agentInfoId, ip);
JSONObject result = new JSONObject();
result.set("inWhitelist", inWhitelist);
return R.ok(result);
}
}
@@ -0,0 +1,34 @@
package com.cool.modules.agent.entity;
import com.cool.core.base.TenantEntity;
import com.mybatisflex.annotation.Table;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Table(value = "agent_info", comment = "代理信息表")
public class AgentInfoEntity extends TenantEntity<AgentInfoEntity> {
@ColumnDefine(comment = "代理名称", notNull = true)
private String name;
@ColumnDefine(comment = "用户名")
private String username;
@ColumnDefine(comment = "密码")
private String password;
@ColumnDefine(comment = "描述", type = "text")
private String description;
@ColumnDefine(comment = "状态 0:停用 1:启用", defaultValue = "1")
private Integer status;
@ColumnDefine(comment = "Token状态 0:失效 1:生效", defaultValue = "0")
private Integer tokenStatus;
@ColumnDefine(comment = "SaySS认证Token(auth_data)")
private String sayssAuthData;
}
@@ -0,0 +1,31 @@
package com.cool.modules.agent.entity;
import com.cool.core.base.TenantEntity;
import com.mybatisflex.annotation.Table;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Table(value = "agent_whitelist", comment = "代理白名单表")
public class AgentWhitelistEntity extends TenantEntity<AgentWhitelistEntity> {
@ColumnDefine(comment = "代理信息ID", notNull = true)
private Long agentInfoId;
@ColumnDefine(comment = "IP地址", notNull = true)
private String ipAddress;
@ColumnDefine(comment = "添加者用户ID")
private Long addedByUserId;
@ColumnDefine(comment = "SaySS白名单记录ID")
private Long sayssTrustedIpId;
@com.mybatisflex.annotation.Column(ignore = true)
private String agentName;
@com.mybatisflex.annotation.Column(ignore = true)
private String addedByName;
}
@@ -0,0 +1,7 @@
package com.cool.modules.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import com.cool.modules.agent.entity.AgentInfoEntity;
public interface AgentInfoMapper extends BaseMapper<AgentInfoEntity> {
}
@@ -0,0 +1,7 @@
package com.cool.modules.agent.mapper;
import com.mybatisflex.core.BaseMapper;
import com.cool.modules.agent.entity.AgentWhitelistEntity;
public interface AgentWhitelistMapper extends BaseMapper<AgentWhitelistEntity> {
}
@@ -0,0 +1,7 @@
package com.cool.modules.agent.service;
import com.cool.core.base.BaseService;
import com.cool.modules.agent.entity.AgentInfoEntity;
public interface AgentInfoService extends BaseService<AgentInfoEntity> {
}
@@ -0,0 +1,7 @@
package com.cool.modules.agent.service;
import com.cool.core.base.BaseService;
import com.cool.modules.agent.entity.AgentWhitelistEntity;
public interface AgentWhitelistService extends BaseService<AgentWhitelistEntity> {
}
@@ -0,0 +1,265 @@
package com.cool.modules.agent.service;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.cool.modules.agent.entity.AgentInfoEntity;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@ConfigurationProperties(prefix = "sayss")
@Data
@RequiredArgsConstructor
public class SayssService {
private String middlewareUrl;
private String middlewareKey;
private String middlewarePath;
private boolean enabled;
private final AgentInfoService agentInfoService;
private String encryptPath(String path, String iv) {
AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, middlewareKey.getBytes(), iv.getBytes());
return aes.encryptBase64(path);
}
private String[] buildEncryptedUrlWithIv(String path) {
String iv = RandomUtil.randomString("0123456789abcdef", 16);
String encrypted = encryptPath(path, iv);
String encoded = Base64.encode(encrypted);
return new String[]{middlewareUrl + middlewarePath + "/" + encoded, iv};
}
private String login(String email, String password) {
if (!enabled) return null;
try {
String[] urlAndIv = buildEncryptedUrlWithIv("/passport/auth/login");
JSONObject body = new JSONObject();
body.set("email", email);
body.set("password", password);
HttpResponse resp = HttpRequest.post(urlAndIv[0])
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/plain, */*")
.header("X-IV", urlAndIv[1])
.body(body.toString())
.timeout(10000).execute();
JSONObject json = JSONUtil.parseObj(resp.body());
if ("success".equals(json.getStr("status")) && json.get("data") != null) {
return json.getJSONObject("data").getStr("auth_data");
}
log.error("SaySS登录失败: {}", json.getStr("message"));
} catch (Exception e) {
log.error("SaySS登录异常: {}", e.getMessage());
}
return null;
}
public String loginAndSave(Long agentInfoId) {
if (!enabled) return null;
AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(agentInfoId);
if (agent == null || StrUtil.isBlank(agent.getUsername()) || StrUtil.isBlank(agent.getPassword())) {
return null;
}
String authData = login(agent.getUsername(), agent.getPassword());
if (authData != null) {
agent.setSayssAuthData(authData);
agent.setTokenStatus(1);
agentInfoService.update(agent);
}
return authData;
}
public boolean checkTokenValid(Long agentInfoId) {
if (!enabled) return false;
AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(agentInfoId);
if (agent == null || StrUtil.isBlank(agent.getSayssAuthData())) {
return false;
}
try {
String[] urlAndIv = buildEncryptedUrlWithIv("/user/info");
HttpResponse resp = HttpRequest.get(urlAndIv[0])
.header("Accept", "application/json, text/plain, */*")
.header("X-IV", urlAndIv[1])
.header("Authorization", agent.getSayssAuthData())
.timeout(10000).execute();
JSONObject json = JSONUtil.parseObj(resp.body());
if ("未登录或登陆已过期".equals(json.getStr("message"))) {
agent.setSayssAuthData(null);
agent.setTokenStatus(0);
agentInfoService.update(agent);
return false;
}
boolean valid = "success".equals(json.getStr("status"));
if (!valid) {
agent.setTokenStatus(0);
agentInfoService.update(agent);
}
return valid;
} catch (Exception e) {
log.error("SaySS验证Token异常: {}", e.getMessage());
return false;
}
}
private JSONObject sayssGet(String authData, String path) {
if (!enabled) return new JSONObject().set("status", "error").set("message", "SaySS未启用");
try {
String[] urlAndIv = buildEncryptedUrlWithIv(path);
HttpResponse resp = HttpRequest.get(urlAndIv[0])
.header("Accept", "application/json, text/plain, */*")
.header("X-IV", urlAndIv[1])
.header("Authorization", authData)
.timeout(10000).execute();
return JSONUtil.parseObj(resp.body());
} catch (Exception e) {
log.error("SaySS GET请求失败: {}", e.getMessage());
return new JSONObject().set("status", "error").set("message", e.getMessage());
}
}
private JSONObject sayssPost(String authData, String path, Object body) {
if (!enabled) return new JSONObject().set("status", "error").set("message", "SaySS未启用");
try {
String[] urlAndIv = buildEncryptedUrlWithIv(path);
HttpResponse resp = HttpRequest.post(urlAndIv[0])
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/plain, */*")
.header("X-IV", urlAndIv[1])
.header("Authorization", authData)
.body(body instanceof String ? (String) body : body.toString())
.timeout(10000).execute();
return JSONUtil.parseObj(resp.body());
} catch (Exception e) {
log.error("SaySS POST请求失败: {}", e.getMessage());
return new JSONObject().set("status", "error").set("message", e.getMessage());
}
}
private JSONObject sayssDelete(String authData, String path) {
if (!enabled) return new JSONObject().set("status", "error").set("message", "SaySS未启用");
try {
String[] urlAndIv = buildEncryptedUrlWithIv(path);
HttpResponse resp = HttpRequest.delete(urlAndIv[0])
.header("Accept", "application/json, text/plain, */*")
.header("X-IV", urlAndIv[1])
.header("Authorization", authData)
.timeout(10000).execute();
return JSONUtil.parseObj(resp.body());
} catch (Exception e) {
log.error("SaySS DELETE请求失败: {}", e.getMessage());
return new JSONObject().set("status", "error").set("message", e.getMessage());
}
}
private String getAuthData(Long agentInfoId) {
AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(agentInfoId);
if (agent == null || StrUtil.isBlank(agent.getUsername()) || StrUtil.isBlank(agent.getPassword())) {
return null;
}
if (StrUtil.isNotBlank(agent.getSayssAuthData())) {
return agent.getSayssAuthData();
}
String authData = login(agent.getUsername(), agent.getPassword());
if (authData != null) {
agent.setSayssAuthData(authData);
agent.setTokenStatus(1);
agentInfoService.update(agent);
}
return authData;
}
public Long addTrustedIp(Long agentInfoId, String ip) {
if (!enabled) {
log.warn("SaySS未启用,跳过添加白名单到SaySS");
return null;
}
String authData = getAuthData(agentInfoId);
if (authData == null) {
log.error("无法获取SaySS认证信息,agentInfoId={}", agentInfoId);
return null;
}
JSONObject body = new JSONObject();
body.set("ip", ip);
body.set("ip_type", "home");
JSONObject result = sayssPost(authData, "/user/subscribe-guard/trusted-ips", body);
if ("success".equals(result.getStr("status")) && result.get("data") != null) {
Object data = result.get("data");
if (data instanceof JSONObject) {
return ((JSONObject) data).getLong("id");
}
}
log.error("SaySS添加白名单失败: {}", result.getStr("message"));
return null;
}
public boolean deleteTrustedIp(Long agentInfoId, Long sayssTrustedIpId) {
if (!enabled) {
log.warn("SaySS未启用,跳过从SaySS删除白名单");
return true;
}
String authData = getAuthData(agentInfoId);
if (authData == null) {
log.error("无法获取SaySS认证信息,agentInfoId={}", agentInfoId);
return false;
}
if (sayssTrustedIpId == null) {
log.warn("SaySS白名单记录ID为空,无法从SaySS删除");
return false;
}
JSONObject result = sayssDelete(authData, "/user/subscribe-guard/trusted-ips/" + sayssTrustedIpId);
if ("success".equals(result.getStr("status"))) {
return true;
}
log.error("SaySS删除白名单失败: {}", result.getStr("message"));
return false;
}
public boolean deleteTrustedIpByIp(Long agentInfoId, String ip) {
if (!enabled) {
log.warn("SaySS未启用,跳过从SaySS删除白名单");
return true;
}
Long trustedIpId = findTrustedIpId(agentInfoId, ip);
if (trustedIpId == null) {
log.warn("在SaySS中未找到IP: {},可能已被删除", ip);
return true;
}
return deleteTrustedIp(agentInfoId, trustedIpId);
}
public Long findTrustedIpId(Long agentInfoId, String ip) {
if (!enabled) return null;
String authData = getAuthData(agentInfoId);
if (authData == null) return null;
JSONObject result = sayssGet(authData, "/user/subscribe-guard/trusted-ips");
if ("success".equals(result.getStr("status")) && result.get("data") != null) {
Object data = result.get("data");
JSONArray list = data instanceof JSONArray ? (JSONArray) data : new JSONArray().put(data);
for (int i = 0; i < list.size(); i++) {
JSONObject item = list.getJSONObject(i);
if (ip.equals(item.getStr("ip_address"))) {
return item.getLong("id");
}
}
}
return null;
}
public boolean isIpInTrustedIps(Long agentInfoId, String ip) {
return findTrustedIpId(agentInfoId, ip) != null;
}
}
@@ -0,0 +1,12 @@
package com.cool.modules.agent.service.impl;
import com.cool.core.base.BaseServiceImpl;
import com.cool.modules.agent.entity.AgentInfoEntity;
import com.cool.modules.agent.mapper.AgentInfoMapper;
import com.cool.modules.agent.service.AgentInfoService;
import org.springframework.stereotype.Service;
@Service
public class AgentInfoServiceImpl extends BaseServiceImpl<AgentInfoMapper, AgentInfoEntity> implements
AgentInfoService {
}
@@ -0,0 +1,12 @@
package com.cool.modules.agent.service.impl;
import com.cool.core.base.BaseServiceImpl;
import com.cool.modules.agent.entity.AgentWhitelistEntity;
import com.cool.modules.agent.mapper.AgentWhitelistMapper;
import com.cool.modules.agent.service.AgentWhitelistService;
import org.springframework.stereotype.Service;
@Service
public class AgentWhitelistServiceImpl extends BaseServiceImpl<AgentWhitelistMapper, AgentWhitelistEntity> implements
AgentWhitelistService {
}
@@ -24,14 +24,12 @@ public class AdminBaseCodingController {
private final BaseCodingService baseCodingService;
@TokenIgnore
@Operation(summary = "获取模块目录结构", description = "获取模块目录结构")
@GetMapping("/getModuleTree")
public R getModuleTree() {
return R.ok(baseCodingService.getModuleTree());
}
@TokenIgnore
@Operation(summary = "创建代码", description = "创建代码")
@PostMapping("/createCode")
public R createCode(@RequestAttribute JSONObject requestParams) {
@@ -6,6 +6,8 @@ import com.cool.core.annotation.TokenIgnore;
import com.cool.core.eps.CoolEps;
import com.cool.core.file.FileUploadStrategyFactory;
import com.cool.core.request.R;
import com.cool.core.util.I18nUtil;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import com.cool.modules.base.service.sys.BaseSysLoginService;
import com.cool.modules.base.service.sys.BaseSysPermsService;
@@ -14,6 +16,7 @@ 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.http.HttpServletRequest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
@@ -41,7 +44,6 @@ public class AdminBaseCommController {
final private FileUploadStrategyFactory fileUploadStrategyFactory;
@TokenIgnore
@Operation(summary = "实体信息与路径", description = "系统所有的实体信息与路径,供前端自动生成代码与服务")
@GetMapping("/eps")
public R eps() {
@@ -67,7 +69,10 @@ public class AdminBaseCommController {
@Operation(summary = "权限与菜单")
@GetMapping("/permmenu")
public R permmenu(@RequestAttribute() Long adminUserId) {
return R.ok(baseSysPermsService.permmenu(adminUserId));
Dict permmenu = baseSysPermsService.permmenu(adminUserId);
List<BaseSysMenuEntity> list = (List<BaseSysMenuEntity>) permmenu.getObj("menus");
list.forEach(o -> o.setName(I18nUtil.getI18nMenu(o.getName())));
return R.ok(permmenu);
}
@Operation(summary = "文件上传")
@@ -13,6 +13,7 @@ import com.cool.core.plugin.service.CoolPluginService;
import com.cool.core.request.R;
import com.cool.core.util.CoolPluginInvokers;
import com.cool.modules.base.dto.sys.BaseSysLoginDto;
import com.cool.modules.base.dto.sys.BaseSysRegisterDto;
import com.cool.modules.base.service.sys.BaseSysLoginService;
import com.cool.modules.plugin.entity.PluginInfoEntity;
import io.micrometer.common.util.StringUtils;
@@ -55,6 +56,12 @@ public class AdminBaseOpenController {
return R.ok(baseSysLoginService.login(baseSysLoginDto));
}
@Operation(summary = "注册")
@PostMapping("/register")
public R register(@RequestBody BaseSysRegisterDto baseSysRegisterDto) {
return R.ok(baseSysLoginService.register(baseSysRegisterDto));
}
@Operation(summary = "验证码")
@GetMapping("/captcha")
public R captcha(@Parameter(description = "类型:svg|base64") @RequestParam(defaultValue = "base64") String type,
@@ -118,4 +125,10 @@ public class AdminBaseOpenController {
}
return R.ok(Map.of("mode", "common"));
}
@Operation(summary = "管理端验证码是否启用")
@GetMapping("/captchaEnabled")
public R captchaEnabled() {
return R.ok(Map.of("enabled", baseSysLoginService.isCaptchaEnabled("admin", "adminLogin")));
}
}
@@ -4,10 +4,12 @@ import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.request.R;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity;
import com.cool.modules.base.service.sys.BaseSysDepartmentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -22,6 +24,13 @@ import java.util.List;
public class AdminBaseSysDepartmentController
extends BaseController<BaseSysDepartmentService, BaseSysDepartmentEntity> {
@Override
protected void checkPermission(String action) {
if (("add".equals(action) || "delete".equals(action) || "update".equals(action)) && !CoolSecurityUtil.isSuperAdmin()) {
throw new AccessDeniedException("仅超级管理员可操作部门");
}
}
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
}
@@ -0,0 +1,76 @@
package com.cool.modules.base.controller.admin.sys;
import static com.cool.modules.base.entity.sys.table.BaseSysEmailConfigEntityTableDef.BASE_SYS_EMAIL_CONFIG_ENTITY;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.request.R;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import com.cool.modules.base.service.sys.BaseSysEmailConfigService;
import com.cool.modules.base.service.sys.EmailSendService;
import com.mybatisflex.core.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
@Tag(name = "邮箱配置管理", description = "邮箱配置管理")
@CoolRestController(api = {"add", "delete", "update", "page", "info"}, cname = "emailConfig")
@RequiredArgsConstructor
public class AdminBaseSysEmailConfigController extends BaseController<BaseSysEmailConfigService, BaseSysEmailConfigEntity> {
private final EmailSendService emailSendService;
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
setPageOption(createOp().fieldEq(BASE_SYS_EMAIL_CONFIG_ENTITY.ENABLED)
.keyWordLikeFields(BASE_SYS_EMAIL_CONFIG_ENTITY.EMAIL, BASE_SYS_EMAIL_CONFIG_ENTITY.REMARK));
}
@Override
protected void checkPermission(String action) {
}
@Operation(summary = "设为当前使用")
@PostMapping("/setActive")
public R setActive(@RequestAttribute JSONObject requestParams) {
Long id = requestParams.getLong("id");
CoolPreconditions.checkEmpty(id, "id不能为空");
BaseSysEmailConfigEntity entity = service.getById(id);
CoolPreconditions.checkEmpty(entity, "邮箱配置不存在");
CoolPreconditions.check(ObjUtil.equals(entity.getEnabled(), 0), "该邮箱配置已禁用,无法设为使用");
List<BaseSysEmailConfigEntity> activeList = service.list(
QueryWrapper.create().eq(BaseSysEmailConfigEntity::getIsActive, 1));
for (BaseSysEmailConfigEntity active : activeList) {
active.setIsActive(0);
active.updateById();
}
entity.setIsActive(1);
entity.updateById();
return R.ok();
}
@Operation(summary = "发送测试邮件")
@PostMapping("/test")
public R test(@RequestAttribute JSONObject requestParams) {
Long id = requestParams.getLong("id");
CoolPreconditions.checkEmpty(id, "id不能为空");
BaseSysEmailConfigEntity entity = service.getById(id);
CoolPreconditions.checkEmpty(entity, "邮箱配置不存在");
emailSendService.sendTestMail(entity);
return R.okMsg("测试邮件发送成功");
}
}
@@ -4,7 +4,9 @@ import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.request.CrudOption;
import com.cool.core.request.R;
import com.cool.core.util.I18nUtil;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.service.sys.BaseSysMenuService;
import io.swagger.v3.oas.annotations.Operation;
@@ -25,6 +27,14 @@ public class AdminBaseSysMenuController extends
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
CrudOption<BaseSysMenuEntity> transform = createOp()
.transformValue(o -> {
BaseSysMenuEntity entity = (BaseSysMenuEntity) o;
entity.setName(I18nUtil.getI18nMenu(entity.getName()));
});
setPageOption(transform);
setListOption(transform);
setInfoOption(transform);
}
@Operation(summary = "创建代码", description = "创建代码")
@@ -6,11 +6,13 @@ import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.base.entity.sys.BaseSysRoleEntity;
import com.cool.modules.base.service.sys.BaseSysRoleService;
import com.mybatisflex.core.query.QueryWrapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.access.AccessDeniedException;
/**
* 系统角色
@@ -19,10 +21,19 @@ import jakarta.servlet.http.HttpServletRequest;
@CoolRestController(api = { "add", "delete", "update", "page", "list", "info" })
public class AdminBaseSysRoleController extends BaseController<BaseSysRoleService, BaseSysRoleEntity> {
@Override
protected void checkPermission(String action) {
if ("add".equals(action) || "delete".equals(action) || "update".equals(action)) {
if (!CoolSecurityUtil.isSuperAdmin()) {
throw new AccessDeniedException("仅超级管理员可操作角色");
}
}
}
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
JSONObject tokenInfo = requestParams.getJSONObject("tokenInfo");
boolean isAdmin = tokenInfo.getStr("username").equals("admin");
boolean isAdmin = CoolSecurityUtil.isSuperAdmin(tokenInfo.getStr("username"));
setPageOption(createOp().keyWordLikeFields(BASE_SYS_ROLE_ENTITY.NAME, BASE_SYS_ROLE_ENTITY.LABEL).queryWrapper(QueryWrapper.create().and(qw -> {
qw.eq(BASE_SYS_ROLE_ENTITY.USER_ID.getName(), tokenInfo.getLong("userId")).or(w -> {
@@ -8,9 +8,11 @@ import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import com.cool.modules.base.service.sys.BaseSysUserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import com.cool.core.util.CoolSecurityUtil;
import jakarta.servlet.http.HttpServletRequest;
/**
@@ -20,6 +22,13 @@ import jakarta.servlet.http.HttpServletRequest;
@CoolRestController(api = { "add", "delete", "update", "page", "info" })
public class AdminBaseSysUserController extends BaseController<BaseSysUserService, BaseSysUserEntity> {
@Override
protected void checkPermission(String action) {
if ("delete".equals(action) && !CoolSecurityUtil.isSuperAdmin()) {
throw new AccessDeniedException("仅超级管理员可删除用户");
}
}
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
}
@@ -21,10 +21,8 @@ public class BaseSysLoginDto {
private String password;
@Schema(description = "验证码ID")
@NotBlank
private String captchaId;
@Schema(description = "验证码")
@NotBlank
private String verifyCode;
}
@@ -0,0 +1,34 @@
package com.cool.modules.base.dto.sys;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
@Data
@Schema(description = "注册参数")
public class BaseSysRegisterDto {
@Schema(description = "用户名")
@NotBlank
private String username;
@Schema(description = "密码")
@NotBlank
private String password;
@Schema(description = "昵称")
private String nickName;
@Schema(description = "手机号")
private String phone;
@Schema(description = "邮箱")
private String email;
@Schema(description = "验证码ID")
private String captchaId;
@Schema(description = "验证码")
private String verifyCode;
}
@@ -2,7 +2,7 @@ package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
@@ -1,9 +1,9 @@
package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.cool.core.base.TenantEntity;
import com.mybatisflex.annotation.Column;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import com.mybatisflex.annotation.Table;
import lombok.Getter;
import lombok.Setter;
@@ -14,7 +14,7 @@ import lombok.Setter;
@Getter
@Setter
@Table(value = "base_sys_department", comment = "系统部门")
public class BaseSysDepartmentEntity extends BaseEntity<BaseSysDepartmentEntity> {
public class BaseSysDepartmentEntity extends TenantEntity<BaseSysDepartmentEntity> {
@ColumnDefine(comment = "部门名称", notNull = true)
private String name;
@@ -24,7 +24,9 @@ public class BaseSysDepartmentEntity extends BaseEntity<BaseSysDepartmentEntity>
@ColumnDefine(comment = "排序", defaultValue = "0")
private Integer orderNum;
// 父菜单名称
@ColumnDefine(comment = "状态 0:禁用 1:启用", defaultValue = "1")
private Integer status;
@Column(ignore = true)
private String parentName;
}
@@ -0,0 +1,40 @@
package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Table(value = "base_sys_email_config", comment = "邮箱配置")
public class BaseSysEmailConfigEntity extends BaseEntity<BaseSysEmailConfigEntity> {
@ColumnDefine(comment = "邮箱地址", notNull = true)
private String email;
@ColumnDefine(comment = "SMTP主机", notNull = true)
private String host;
@ColumnDefine(comment = "SMTP端口", notNull = true)
private Integer port;
@ColumnDefine(comment = "用户名", notNull = true)
private String username;
@ColumnDefine(comment = "密码/授权码", notNull = true)
private String password;
@ColumnDefine(comment = "是否启用SSL", defaultValue = "1")
private Integer sslEnable;
@ColumnDefine(comment = "是否启用 0-禁用 1-启用", defaultValue = "1")
private Integer enabled;
@ColumnDefine(comment = "是否为当前使用 0-否 1-是", defaultValue = "0")
private Integer isActive;
@ColumnDefine(comment = "备注")
private String remark;
}
@@ -3,8 +3,8 @@ package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.Fastjson2TypeHandler;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.mybatis.handler.Fastjson2TypeHandler;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
@@ -3,7 +3,7 @@ package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@@ -2,7 +2,7 @@ package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
@@ -0,0 +1,41 @@
package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.cool.core.annotation.ColumnDefine;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import com.cool.core.mybatis.handler.Fastjson2TypeHandler;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
@Getter
@Setter
@Table(value = "base_sys_perms_audit_log", comment = "权限变更审计日志表")
public class BaseSysPermsAuditLogEntity extends BaseEntity<BaseSysPermsAuditLogEntity> {
@Index
@ColumnDefine(comment = "操作用户ID", type = "bigint")
private Long operatorUserId;
@ColumnDefine(comment = "操作用户名")
private String operatorUsername;
@ColumnDefine(comment = "变更类型: ROLE_ASSIGN/PERMS_UPDATE/ROLE_DISABLE/ROLE_ENABLE/USER_ROLE_UPDATE")
private String changeType;
@Index
@ColumnDefine(comment = "目标ID(角色ID或用户ID)", type = "bigint")
private Long targetId;
@ColumnDefine(comment = "变更前值", type = "json")
@Column(typeHandler = Fastjson2TypeHandler.class)
private Object beforeValue;
@ColumnDefine(comment = "变更后值", type = "json")
@Column(typeHandler = Fastjson2TypeHandler.class)
private Object afterValue;
@ColumnDefine(comment = "IP地址", length = 50)
private String ip;
}
@@ -2,7 +2,7 @@ package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import com.mybatisflex.annotation.Table;
import lombok.Getter;
import lombok.Setter;
@@ -1,10 +1,10 @@
package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.cool.core.base.TenantEntity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import com.mybatisflex.core.handler.Fastjson2TypeHandler;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.mybatis.handler.Fastjson2TypeHandler;
import com.cool.core.annotation.ColumnDefine;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@@ -14,7 +14,7 @@ import org.dromara.autotable.annotation.enums.IndexTypeEnum;
@Getter
@Setter
@Table(value = "base_sys_role", comment = "系统角色表")
public class BaseSysRoleEntity extends BaseEntity<BaseSysRoleEntity> {
public class BaseSysRoleEntity extends TenantEntity<BaseSysRoleEntity> {
@Index
@ColumnDefine(comment = "用户ID", notNull = true, type = "bigint")
@@ -30,9 +30,15 @@ public class BaseSysRoleEntity extends BaseEntity<BaseSysRoleEntity> {
@ColumnDefine(comment = "备注")
private String remark;
@ColumnDefine(comment = "状态 0:禁用 1:启用", defaultValue = "1")
private Integer status;
@ColumnDefine(comment = "数据权限是否关联上下级", defaultValue = "1")
private Integer relevance;
@ColumnDefine(comment = "父角色ID", type = "bigint")
private Long parentId;
@ColumnDefine(comment = "菜单权限", type = "json")
@Column(typeHandler = Fastjson2TypeHandler.class)
private List<Long> menuIdList;
@@ -2,7 +2,7 @@ package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import com.mybatisflex.annotation.Table;
import lombok.Getter;
import lombok.Setter;
@@ -1,18 +1,22 @@
package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.cool.core.base.TenantEntity;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Ignore;
import org.dromara.autotable.annotation.Index;
import org.dromara.autotable.annotation.enums.IndexTypeEnum;
import java.util.List;
@Getter
@Setter
@Table(value = "base_sys_user", comment = "系统用户表")
public class BaseSysUserEntity extends BaseEntity<BaseSysUserEntity> {
public class BaseSysUserEntity extends TenantEntity<BaseSysUserEntity> {
@Index
@ColumnDefine(comment = "部门ID", type = "bigint")
private Long departmentId;
@@ -58,4 +62,11 @@ public class BaseSysUserEntity extends BaseEntity<BaseSysUserEntity> {
@ColumnDefine(comment = "socketId")
private String socketId;
@Ignore
@Schema( description = "角色列表" )
private List<Long> roleIdList;
}
@@ -2,7 +2,7 @@ package com.cool.modules.base.entity.sys;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import lombok.Getter;
import lombok.Setter;
import org.dromara.autotable.annotation.Index;
@@ -0,0 +1,7 @@
package com.cool.modules.base.mapper.sys;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import com.mybatisflex.core.BaseMapper;
public interface BaseSysEmailConfigMapper extends BaseMapper<BaseSysEmailConfigEntity> {
}
@@ -0,0 +1,7 @@
package com.cool.modules.base.mapper.sys;
import com.mybatisflex.core.BaseMapper;
import com.cool.modules.base.entity.sys.BaseSysPermsAuditLogEntity;
public interface BaseSysPermsAuditLogMapper extends BaseMapper<BaseSysPermsAuditLogEntity> {
}
@@ -47,6 +47,13 @@ public class MySecurityMetadataSource implements FilterInvocationSecurityMetadat
}
}
/**
* 清除权限映射缓存,下次请求时重新加载
*/
public void clearCache() {
map = null;
}
/**
* 判定用户请求的url是否在权限表中 如果在权限表中,则返回给decide方法,用来判定用户是否有此权限 如果不在权限表中则放行
*
@@ -0,0 +1,9 @@
package com.cool.modules.base.service.sys;
import com.cool.core.base.BaseService;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
public interface BaseSysEmailConfigService extends BaseService<BaseSysEmailConfigEntity> {
BaseSysEmailConfigEntity getActiveConfig();
}
@@ -2,6 +2,7 @@ package com.cool.modules.base.service.sys;
import com.cool.core.enums.UserTypeEnum;
import com.cool.modules.base.dto.sys.BaseSysLoginDto;
import com.cool.modules.base.dto.sys.BaseSysRegisterDto;
/**
* 系统登录
@@ -22,6 +23,8 @@ public interface BaseSysLoginService {
*/
void captchaCheck(String captchaId, String code);
boolean isCaptchaEnabled(String side, String sceneKey);
/**
* 登录
*
@@ -45,4 +48,12 @@ public interface BaseSysLoginService {
* @return 新的token
*/
Object refreshToken(String refreshToken);
/**
* 注册
*
* @param baseSysRegisterDto 注册必要信息
* @return token与相关的过期信息
*/
Object register(BaseSysRegisterDto baseSysRegisterDto);
}
@@ -1,8 +1,8 @@
package com.cool.modules.base.service.sys;
import cn.hutool.core.lang.Dict;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import java.util.List;
/**
@@ -88,6 +88,14 @@ public interface BaseSysPermsService {
*/
Long[] getDepartmentIdsByRoleIds(Long[] roleIds);
/**
* 根据用户ID获得部门ID
*
* @param userId 角色ID数组
* @return 部门ID数组
*/
Long[] getDepartmentIdsByUserId(Long userId);
/**
* 根据用户ID获得部门ID
*
@@ -117,7 +125,7 @@ public interface BaseSysPermsService {
* @param adminUserId 登录的用户
* @return 权限菜单
*/
Object permmenu(Long adminUserId);
Dict permmenu(Long adminUserId);
/**
* 更新角色权限
@@ -157,4 +165,18 @@ public interface BaseSysPermsService {
*/
void refreshPermsByRoleId(Long roleId);
/**
* 校验当前用户是否有权访问指定部门的数据
*
* @param departmentId 部门ID
* @return true=有权访问
*/
boolean hasDepartmentPermission(Long departmentId);
/**
* 获取当前用户可访问的部门ID集合,超管返回null表示全部
*
* @return 部门ID数组,超管返回null
*/
Long[] getCurrentUserDepartmentIds();
}
@@ -0,0 +1,90 @@
package com.cool.modules.base.service.sys;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailSendService {
private final BaseSysEmailConfigService emailConfigService;
public void sendCode(String toEmail, String code) {
BaseSysEmailConfigEntity config = emailConfigService.getActiveConfig();
if (config == null) {
throw new RuntimeException("未配置可用的邮箱服务,请在系统管理中配置邮箱");
}
JavaMailSender mailSender = buildMailSender(config);
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(config.getEmail());
helper.setTo(toEmail);
helper.setSubject("验证码");
helper.setText("<div style=\"padding:20px;font-family:sans-serif;\">"
+ "<h2>验证码</h2>"
+ "<p>您的验证码为:<strong style=\"font-size:24px;color:#14b8a6;\">" + code + "</strong></p>"
+ "<p style=\"color:#999;font-size:12px;\">验证码10分钟内有效,请勿泄露给他人。</p>"
+ "</div>", true);
mailSender.send(message);
log.info("验证码邮件发送成功: to={}", toEmail);
} catch (Exception e) {
log.error("验证码邮件发送失败: to={}, error={}", toEmail, e.getMessage());
throw new RuntimeException("邮件发送失败,请检查邮箱配置");
}
}
public void sendTestMail(BaseSysEmailConfigEntity config) {
JavaMailSender mailSender = buildMailSender(config);
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(config.getEmail());
helper.setTo(config.getEmail());
helper.setSubject("测试邮件");
helper.setText("<div style=\"padding:20px;font-family:sans-serif;\">"
+ "<h2>测试邮件</h2>"
+ "<p>这是一封测试邮件,如果您收到了说明邮箱配置正确。</p>"
+ "<p style=\"color:#999;font-size:12px;\">发送时间:" + java.time.LocalDateTime.now() + "</p>"
+ "</div>", true);
mailSender.send(message);
log.info("测试邮件发送成功: to={}", config.getEmail());
} catch (Exception e) {
log.error("测试邮件发送失败: to={}, error={}", config.getEmail(), e.getMessage());
throw new RuntimeException("邮件发送失败,请检查邮箱配置:" + e.getMessage());
}
}
private JavaMailSender buildMailSender(BaseSysEmailConfigEntity config) {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost(config.getHost());
sender.setPort(config.getPort());
sender.setUsername(config.getUsername());
sender.setPassword(config.getPassword());
sender.setDefaultEncoding("UTF-8");
java.util.Properties props = sender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
if (config.getSslEnable() != null && config.getSslEnable() == 1) {
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
} else {
props.put("mail.smtp.starttls.enable", "true");
}
props.put("mail.smtp.timeout", "5000");
props.put("mail.smtp.connectiontimeout", "5000");
return sender;
}
}
@@ -3,6 +3,7 @@ package com.cool.modules.base.service.sys.impl;
import com.cool.core.exception.CoolPreconditions;
import com.cool.modules.base.dto.sys.CodeContentDto;
import com.cool.modules.base.service.sys.BaseCodingService;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
@@ -11,11 +12,10 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class BaseCodingServiceImpl implements BaseCodingService {
@@ -69,6 +69,8 @@ public class BaseCodingServiceImpl implements BaseCodingService {
}
// 写入文件
try (FileWriter writer = new FileWriter(filePath.toFile())) {
formattedContent = formattedContent.replace("com.tangzc.mybatisflex.autotable.annotation.Index;",
"org.dromara.autotable.annotation.Index;");
writer.write(formattedContent);
}
list.add(filePath.toString());
@@ -2,10 +2,13 @@ package com.cool.modules.base.service.sys.impl;
import cn.hutool.json.JSONObject;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.exception.CoolPreconditions;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity;
import com.cool.modules.base.entity.sys.BaseSysRoleDepartmentEntity;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import com.cool.modules.base.mapper.sys.BaseSysDepartmentMapper;
import com.cool.modules.base.mapper.sys.BaseSysRoleDepartmentMapper;
import com.cool.modules.base.mapper.sys.BaseSysUserMapper;
import com.cool.modules.base.service.sys.BaseSysDepartmentService;
import com.cool.modules.base.service.sys.BaseSysPermsService;
@@ -15,6 +18,7 @@ import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 系统部门
@@ -29,6 +33,8 @@ public class BaseSysDepartmentServiceImpl extends
final private BaseSysPermsService baseSysPermsService;
final private BaseSysRoleDepartmentMapper baseSysRoleDepartmentMapper;
@Override
public void order(List<BaseSysDepartmentEntity> list) {
list.forEach(baseSysDepartmentEntity -> {
@@ -48,7 +54,7 @@ public class BaseSysDepartmentServiceImpl extends
}
List<BaseSysDepartmentEntity> list = this.list(
QueryWrapper.create()
.in(BaseSysDepartmentEntity::getId, loginDepartmentIds, !username.equals("admin"))
.in(BaseSysDepartmentEntity::getId, loginDepartmentIds, !CoolSecurityUtil.isSuperAdmin(username))
.orderBy(BaseSysDepartmentEntity::getOrderNum, false));
list.forEach(e -> {
List<BaseSysDepartmentEntity> parentDepartment = list.stream()
@@ -63,22 +69,26 @@ public class BaseSysDepartmentServiceImpl extends
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean delete(JSONObject requestParams, Long... ids) {
super.delete(ids);
// 是否删除对应用户 否则移动到顶层部门
if (requestParams.getBool("deleteUser")) {
return baseSysUserMapper
.deleteByQuery(
QueryWrapper.create().in(BaseSysUserEntity::getDepartmentId, (Object) ids)) > 0;
} else {
BaseSysDepartmentEntity topDepartment = getOne(
QueryWrapper.create().isNull(BaseSysDepartmentEntity::getParentId));
if (topDepartment != null) {
UpdateChain.of(BaseSysUserEntity.class)
.set(BaseSysUserEntity::getDepartmentId, topDepartment.getId())
.in(BaseSysUserEntity::getDepartmentId, (Object) ids).update();
}
List<Long> allDeptIds = new ArrayList<>(List.of(ids));
for (Long id : ids) {
collectChildDeptIds(id, allDeptIds);
}
long userCount = baseSysUserMapper.selectCountByQuery(
QueryWrapper.create().in(BaseSysUserEntity::getDepartmentId, allDeptIds));
CoolPreconditions.check(userCount > 0, "该部门或其子部门下存在用户,无法删除");
baseSysRoleDepartmentMapper.deleteByQuery(
QueryWrapper.create().in(BaseSysRoleDepartmentEntity::getDepartmentId, allDeptIds));
return super.delete(ids);
}
private void collectChildDeptIds(Long parentId, List<Long> result) {
List<BaseSysDepartmentEntity> children = list(
QueryWrapper.create().eq(BaseSysDepartmentEntity::getParentId, parentId));
for (BaseSysDepartmentEntity child : children) {
result.add(child.getId());
collectChildDeptIds(child.getId(), result);
}
return false;
}
}
@@ -0,0 +1,32 @@
package com.cool.modules.base.service.sys.impl;
import cn.hutool.core.util.ObjUtil;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.exception.CoolPreconditions;
import com.cool.modules.base.entity.sys.BaseSysEmailConfigEntity;
import com.cool.modules.base.mapper.sys.BaseSysEmailConfigMapper;
import com.cool.modules.base.service.sys.BaseSysEmailConfigService;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
@Service
public class BaseSysEmailConfigServiceImpl extends BaseServiceImpl<BaseSysEmailConfigMapper, BaseSysEmailConfigEntity>
implements BaseSysEmailConfigService {
@Override
public BaseSysEmailConfigEntity getActiveConfig() {
return getOne(QueryWrapper.create()
.eq(BaseSysEmailConfigEntity::getIsActive, 1)
.eq(BaseSysEmailConfigEntity::getEnabled, 1));
}
@Override
public void modifyAfter(cn.hutool.json.JSONObject requestParams, BaseSysEmailConfigEntity entity, com.cool.core.base.ModifyEnum type) {
if (type == com.cool.core.base.ModifyEnum.ADD || type == com.cool.core.base.ModifyEnum.UPDATE) {
if (ObjUtil.isNotEmpty(entity.getIsActive()) && entity.getIsActive() == 1) {
CoolPreconditions.check(entity.getId() == null && type == com.cool.core.base.ModifyEnum.UPDATE,
"新增时无法设置活跃状态");
}
}
}
}
@@ -15,10 +15,16 @@ import com.cool.core.exception.CoolPreconditions;
import com.cool.core.security.jwt.JwtTokenUtil;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.base.dto.sys.BaseSysLoginDto;
import com.cool.modules.base.dto.sys.BaseSysRegisterDto;import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity;
import com.cool.modules.base.entity.sys.BaseSysRoleEntity;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import com.cool.modules.base.mapper.sys.BaseSysDepartmentMapper;
import com.cool.modules.base.mapper.sys.BaseSysRoleMapper;
import com.cool.modules.base.mapper.sys.BaseSysUserMapper;
import com.cool.modules.base.service.sys.BaseSysLoginService;
import com.cool.modules.base.service.sys.BaseSysPermsService;
import com.cool.modules.user.entity.UserCaptchaConfigEntity;
import com.cool.modules.user.service.UserCaptchaConfigService;
import com.mybatisflex.core.query.QueryWrapper;
import java.awt.Color;
import java.util.HashMap;
@@ -42,8 +48,19 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
private final BaseSysUserMapper baseSysUserMapper;
private final BaseSysRoleMapper baseSysRoleMapper;
private final BaseSysDepartmentMapper baseSysDepartmentMapper;
private final BaseSysPermsService baseSysPermsService;
private final UserCaptchaConfigService userCaptchaConfigService;
private final org.springframework.security.crypto.password.PasswordEncoder passwordEncoder;
@org.springframework.beans.factory.annotation.Value("${cool.captcha.enable:true}")
private boolean captchaEnable;
@Override
public Object captcha(UserTypeEnum userTypeEnum, String type, Integer width, Integer height) {
// 1、生成验证码 2、生成对应的ID并设置在缓存中,验证码过期时间30分钟;
@@ -77,10 +94,23 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
}
}
@Override
public boolean isCaptchaEnabled(String side, String sceneKey) {
UserCaptchaConfigEntity config = userCaptchaConfigService.getOne(
QueryWrapper.create()
.eq(UserCaptchaConfigEntity::getSide, side)
.eq(UserCaptchaConfigEntity::getSceneKey, sceneKey));
if (config != null) {
return ObjUtil.equals(config.getEnabled(), 1);
}
return captchaEnable;
}
@Override
public Object login(BaseSysLoginDto baseSysLoginDto) {
// 1、检查验证码是否正确 2、执行登录操作
captchaCheck(baseSysLoginDto.getCaptchaId(), baseSysLoginDto.getVerifyCode());
if (isCaptchaEnabled("admin", "adminLogin")) {
captchaCheck(baseSysLoginDto.getCaptchaId(), baseSysLoginDto.getVerifyCode());
}
UsernamePasswordAuthenticationToken upToken =
new UsernamePasswordAuthenticationToken(
baseSysLoginDto.getUsername(), baseSysLoginDto.getPassword());
@@ -94,7 +124,9 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
CoolPreconditions.check(
ObjectUtil.isEmpty(baseSysUserEntity) || baseSysUserEntity.getStatus() == 0, "用户已禁用");
Long[] roleIds = baseSysPermsService.getRoles(baseSysUserEntity);
coolCache.del("verify:img:" + baseSysLoginDto.getCaptchaId());
if (isCaptchaEnabled("admin", "adminLogin")) {
coolCache.del("verify:img:" + baseSysLoginDto.getCaptchaId());
}
return generateToken(roleIds, baseSysUserEntity, null);
}
@@ -115,6 +147,40 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
return generateToken(roleIds, baseSysUserEntity, refreshToken);
}
@Override
public Object register(BaseSysRegisterDto baseSysRegisterDto) {
if (isCaptchaEnabled("admin", "adminRegister")) {
captchaCheck(baseSysRegisterDto.getCaptchaId(), baseSysRegisterDto.getVerifyCode());
}
BaseSysUserEntity existUser = baseSysUserMapper.selectOneByQuery(
QueryWrapper.create().eq(BaseSysUserEntity::getUsername, baseSysRegisterDto.getUsername()));
CoolPreconditions.check(existUser != null, "用户名已存在");
BaseSysRoleEntity normalRole = baseSysRoleMapper.selectOneByQuery(
QueryWrapper.create().eq(BaseSysRoleEntity::getLabel, "user"));
CoolPreconditions.check(normalRole == null, "系统未配置普通用户角色,请联系管理员");
BaseSysDepartmentEntity defaultDepartment = baseSysDepartmentMapper.selectOneByQuery(
QueryWrapper.create().eq(BaseSysDepartmentEntity::getName, "待分配部门")
.eq(BaseSysDepartmentEntity::getStatus, 1));
CoolPreconditions.check(defaultDepartment == null, "系统未配置部门,请联系管理员");
BaseSysUserEntity userEntity = new BaseSysUserEntity();
userEntity.setUsername(baseSysRegisterDto.getUsername());
userEntity.setPassword(passwordEncoder.encode(baseSysRegisterDto.getPassword()));
userEntity.setPasswordV(1);
userEntity.setNickName(StrUtil.isNotEmpty(baseSysRegisterDto.getNickName())
? baseSysRegisterDto.getNickName() : baseSysRegisterDto.getUsername());
userEntity.setName(userEntity.getNickName());
userEntity.setPhone(baseSysRegisterDto.getPhone());
userEntity.setEmail(baseSysRegisterDto.getEmail());
userEntity.setStatus(1);
userEntity.setDepartmentId(defaultDepartment.getId());
baseSysUserMapper.insertSelective(userEntity);
baseSysPermsService.updateUserRole(userEntity.getId(), new Long[]{normalRole.getId()});
if (isCaptchaEnabled("admin", "adminRegister")) {
coolCache.del("verify:img:" + baseSysRegisterDto.getCaptchaId());
}
return Dict.create().set("message", "注册成功");
}
private Dict generateToken(Long[] roleIds, BaseSysUserEntity baseSysUserEntity, String refreshToken) {
Dict tokenInfo =
Dict.create()
@@ -122,6 +188,7 @@ public class BaseSysLoginServiceImpl implements BaseSysLoginService {
.set("roleIds", roleIds)
.set("username", baseSysUserEntity.getUsername())
.set("userId", baseSysUserEntity.getId())
.set("tenantId", baseSysUserEntity.getTenantId())
.set("passwordVersion", baseSysUserEntity.getPasswordV());
String token = jwtTokenUtil.generateToken(tokenInfo);
if (StrUtil.isEmpty(refreshToken)) {
@@ -3,16 +3,20 @@ package com.cool.modules.base.service.sys.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import com.cool.CoolApplication;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.base.ModifyEnum;
import com.cool.core.eps.CoolEps;
import com.cool.core.i18n.I18nGenerator;
import com.cool.core.util.CompilerUtils;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.core.util.PathUtils;
import com.cool.modules.base.entity.sys.BaseSysMenuEntity;
import com.cool.modules.base.entity.sys.BaseSysRoleMenuEntity;
import com.cool.modules.base.mapper.sys.BaseSysMenuMapper;
import com.cool.modules.base.mapper.sys.BaseSysRoleMenuMapper;
import com.cool.modules.base.service.sys.BaseSysMenuService;
import com.cool.modules.base.service.sys.BaseSysPermsService;
import com.mybatisflex.core.query.QueryWrapper;
@@ -35,6 +39,8 @@ public class BaseSysMenuServiceImpl extends BaseServiceImpl<BaseSysMenuMapper, B
final private CoolEps coolEps;
final private BaseSysRoleMenuMapper baseSysRoleMenuMapper;
@Override
public Object list(JSONObject requestParams, QueryWrapper queryWrapper) {
List<BaseSysMenuEntity> list = baseSysPermsService.getMenus(CoolSecurityUtil.getAdminUsername());
@@ -62,10 +68,17 @@ public class BaseSysMenuServiceImpl extends BaseServiceImpl<BaseSysMenuMapper, B
baseSysPermsService.refreshPermsByMenuId(id);
}
}
if (ModifyEnum.ADD.equals(type) || ModifyEnum.UPDATE.equals(type)) {
SpringUtil.getBean(I18nGenerator.class).asyncGenBaseMenu();
}
}
@Override
public boolean delete(Long... ids) {
for (Long id : ids) {
baseSysRoleMenuMapper.deleteByQuery(
QueryWrapper.create().eq(BaseSysRoleMenuEntity::getMenuId, id));
}
super.delete(ids);
for (Long id : ids) {
this.delChildMenu(id);
@@ -126,6 +139,7 @@ public class BaseSysMenuServiceImpl extends BaseServiceImpl<BaseSysMenuMapper, B
@Override
public boolean importMenu(List<BaseSysMenuEntity> menus) {
menus.forEach(this::importMenu);
SpringUtil.getBean(I18nGenerator.class).asyncGenBaseMenu();
return true;
}
@@ -15,16 +15,21 @@ import com.cool.core.util.CoolSecurityUtil;
import com.cool.core.util.SpringContextUtils;
import com.cool.modules.base.entity.sys.*;
import com.cool.modules.base.mapper.sys.*;
import com.cool.modules.base.security.MySecurityMetadataSource;
import com.cool.modules.base.service.sys.BaseSysPermsService;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.row.Row;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@@ -38,16 +43,20 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
final private BaseSysRoleMenuMapper baseSysRoleMenuMapper;
final private BaseSysRoleMapper baseSysRoleMapper;
final private BaseSysRoleDepartmentMapper baseSysRoleDepartmentMapper;
final private BaseSysDepartmentMapper baseSysDepartmentMapper;
final private ExecutorService cachedThreadPool;
final private BaseSysPermsAuditLogMapper baseSysPermsAuditLogMapper;
@Override
public Long[] loginDepartmentIds() {
String username = CoolSecurityUtil.getAdminUsername();
if (username.equals("admin")) {
if (CoolSecurityUtil.isSuperAdmin(username)) {
return baseSysDepartmentMapper.selectAll().stream().map(BaseSysDepartmentEntity::getId)
.toArray(Long[]::new);
} else {
@@ -72,9 +81,30 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
if (!CollUtil.toList(roleIds).contains(1L)) {
queryWrapper.in(BaseSysRoleDepartmentEntity::getRoleId, (Object) roleIds);
}
return baseSysRoleDepartmentMapper
List<Long> deptIds = baseSysRoleDepartmentMapper
.selectListByQuery(queryWrapper)
.stream().map(BaseSysRoleDepartmentEntity::getDepartmentId).toArray(Long[]::new);
.stream().map(BaseSysRoleDepartmentEntity::getDepartmentId).collect(java.util.stream.Collectors.toList());
Set<Long> expandedDeptIds = new LinkedHashSet<>(deptIds);
for (Long roleId : roleIds) {
if (roleId == 1L) continue;
BaseSysRoleEntity role = baseSysRoleMapper.selectOneById(roleId);
if (role != null && role.getRelevance() != null && role.getRelevance() == 1) {
for (Long deptId : deptIds) {
collectChildDeptIds(deptId, expandedDeptIds);
}
}
}
return expandedDeptIds.toArray(Long[]::new);
}
private void collectChildDeptIds(Long parentId, Set<Long> result) {
List<BaseSysDepartmentEntity> children = baseSysDepartmentMapper
.selectListByQuery(QueryWrapper.create().eq(BaseSysDepartmentEntity::getParentId, parentId));
for (BaseSysDepartmentEntity child : children) {
if (result.add(child.getId())) {
collectChildDeptIds(child.getId(), result);
}
}
}
@Override
@@ -83,6 +113,11 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
return getLongs(roleIds);
}
@Override
public Long[] getDepartmentIdsByUserId(Long userId) {
return (Long[]) coolCache.get("admin:department:" + userId);
}
@Override
public String[] getPermsCache(Long userId) {
Object result = coolCache.get("admin:perms:" + userId);
@@ -106,11 +141,18 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
public Long[] getRoles(BaseSysUserEntity userEntity) {
Long[] roleIds = null;
if (!userEntity.getUsername().equals("admin")) {
if (!CoolSecurityUtil.isSuperAdmin(userEntity.getUsername())) {
List<BaseSysUserRoleEntity> list = baseSysUserRoleMapper
.selectListByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, userEntity.getId()));
roleIds = list.stream().map(BaseSysUserRoleEntity::getRoleId).toArray(Long[]::new);
if (Arrays.asList(roleIds).contains(1L)) {
List<Long> activeRoleIds = new ArrayList<>();
for (BaseSysUserRoleEntity userRole : list) {
BaseSysRoleEntity role = baseSysRoleMapper.selectOneById(userRole.getRoleId());
if (role != null && role.getStatus() != 0) {
activeRoleIds.add(userRole.getRoleId());
}
}
roleIds = activeRoleIds.toArray(Long[]::new);
if (roleIds.length > 0 && Arrays.asList(roleIds).contains(1L)) {
roleIds = null;
}
}
@@ -124,7 +166,7 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
public String[] getPerms(Long[] roleIds) {
List<BaseSysMenuEntity> menus = getMenus(roleIds);
List<BaseSysMenuEntity> menus = getMenus(expandWithParentRoles(roleIds));
Set<String> perms = new HashSet<>();
String[] permsData = menus.stream().map(BaseSysMenuEntity::getPerms)
.filter(itemPerms -> !StrUtil.isEmpty(itemPerms)).toArray(String[]::new);
@@ -136,6 +178,10 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
public List<BaseSysMenuEntity> getMenus(Long[] roleIds) {
return getMenusInner(expandWithParentRoles(roleIds));
}
private List<BaseSysMenuEntity> getMenusInner(Long[] roleIds) {
if (CollUtil.toList(roleIds).contains(1L)) {
roleIds = null;
}
@@ -168,12 +214,19 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
}
@Override
public Object permmenu(Long adminUserId) {
public Dict permmenu(Long adminUserId) {
return Dict.create().set("menus", getMenus(adminUserId)).set("perms", getPerms(adminUserId));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updatePerms(Long roleId, Long[] menuIdList, Long[] departmentIds) {
Long[] beforeMenuIds = baseSysRoleMenuMapper.selectListByQuery(
QueryWrapper.create().eq(BaseSysRoleMenuEntity::getRoleId, roleId))
.stream().map(BaseSysRoleMenuEntity::getMenuId).toArray(Long[]::new);
Long[] beforeDeptIds = baseSysRoleDepartmentMapper.selectListByQuery(
QueryWrapper.create().eq(BaseSysRoleDepartmentEntity::getRoleId, roleId))
.stream().map(BaseSysRoleDepartmentEntity::getDepartmentId).toArray(Long[]::new);
// 更新菜单权限
baseSysRoleMenuMapper.deleteByQuery(QueryWrapper.create().eq(BaseSysRoleMenuEntity::getRoleId, roleId));
List<BaseSysRoleMenuEntity> batchRoleMenuList = new ArrayList<>();
@@ -199,6 +252,10 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
if (ObjectUtil.isNotEmpty(batchRoleDepartmentList)) {
baseSysRoleDepartmentMapper.insertBatch(batchRoleDepartmentList);
}
SpringContextUtils.getBean(MySecurityMetadataSource.class).clearCache();
logPermsAudit("PERMS_UPDATE", roleId,
Dict.create().set("menuIds", beforeMenuIds).set("deptIds", beforeDeptIds),
Dict.create().set("menuIds", menuIdList).set("deptIds", departmentIds));
cachedThreadPool.submit(() -> {
// 刷新对应角色用户的权限
List<BaseSysUserRoleEntity> userRoles = baseSysUserRoleMapper
@@ -210,7 +267,11 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateUserRole(Long userId, Long[] roleIdList) {
Long[] beforeRoleIds = baseSysUserRoleMapper.selectListByQuery(
QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, userId))
.stream().map(BaseSysUserRoleEntity::getRoleId).toArray(Long[]::new);
baseSysUserRoleMapper.deleteByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, userId));
if (roleIdList == null) {
roleIdList = new Long[0];
@@ -221,6 +282,9 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
sysUserRoleEntity.setUserId(userId);
baseSysUserRoleMapper.insert(sysUserRoleEntity);
}
logPermsAudit("USER_ROLE_UPDATE", userId,
Dict.create().set("roleIds", beforeRoleIds),
Dict.create().set("roleIds", roleIdList));
refreshPerms(userId);
}
@@ -238,10 +302,13 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Async
@Override
public void refreshPermsByMenuId(Long menuId) {
// 刷新超管权限、 找出这个菜单的所有用户、 刷新用户权限
BaseSysUserEntity admin = baseSysUserMapper
.selectOneByQuery(QueryWrapper.create().eq(BaseSysUserEntity::getUsername, "admin"));
refreshPerms(admin.getId());
SpringContextUtils.getBean(MySecurityMetadataSource.class).clearCache();
List<BaseSysUserEntity> superAdmins = baseSysUserMapper
.selectListByQuery(QueryWrapper.create()
.eq(BaseSysUserEntity::getUsername, "admin"));
for (BaseSysUserEntity admin : superAdmins) {
refreshPerms(admin.getId());
}
List<Row> list = baseSysRoleMenuMapper.selectRowsByQuery(QueryWrapper.create().select(BASE_SYS_USER_ROLE_ENTITY.USER_ID)
.from(BASE_SYS_ROLE_MENU_ENTITY).leftJoin(BASE_SYS_USER_ROLE_ENTITY)
.on(BASE_SYS_ROLE_MENU_ENTITY.ROLE_ID.eq(BASE_SYS_USER_ROLE_ENTITY.ROLE_ID)).and(BASE_SYS_ROLE_MENU_ENTITY.MENU_ID.eq(menuId, ObjectUtil.isNotEmpty(menuId))).groupBy(BASE_SYS_USER_ROLE_ENTITY.USER_ID));
@@ -252,11 +319,68 @@ public class BaseSysPermsServiceImpl implements BaseSysPermsService {
@Override
public void refreshPermsByRoleId(Long roleId) {
// 找出角色对应的所有用户
List<BaseSysUserRoleEntity> list = baseSysUserRoleMapper
.selectListByQuery(QueryWrapper.create().eq(BaseSysUserRoleEntity::getRoleId, roleId));
list.forEach(e -> {
refreshPerms(e.getUserId());
});
}
@Override
public boolean hasDepartmentPermission(Long departmentId) {
if (CoolSecurityUtil.isSuperAdmin()) {
return true;
}
Long[] permittedIds = getDepartmentIdsByUserId(CoolSecurityUtil.getCurrentUserId());
if (ObjectUtil.isEmpty(permittedIds)) {
return false;
}
return Arrays.asList(permittedIds).contains(departmentId);
}
@Override
public Long[] getCurrentUserDepartmentIds() {
if (CoolSecurityUtil.isSuperAdmin()) {
return null;
}
return getDepartmentIdsByUserId(CoolSecurityUtil.getCurrentUserId());
}
private Long[] expandWithParentRoles(Long[] roleIds) {
if (ObjectUtil.isEmpty(roleIds)) {
return roleIds;
}
Set<Long> expanded = new LinkedHashSet<>(Arrays.asList(roleIds));
for (Long roleId : roleIds) {
collectParentRoleIds(roleId, expanded);
}
return expanded.toArray(Long[]::new);
}
private void collectParentRoleIds(Long roleId, Set<Long> result) {
BaseSysRoleEntity role = baseSysRoleMapper.selectOneById(roleId);
if (role == null || role.getParentId() == null || result.contains(role.getParentId())) {
return;
}
BaseSysRoleEntity parentRole = baseSysRoleMapper.selectOneById(role.getParentId());
if (parentRole != null && parentRole.getStatus() != 0) {
result.add(parentRole.getId());
collectParentRoleIds(parentRole.getId(), result);
}
}
private void logPermsAudit(String changeType, Long targetId, Object beforeValue, Object afterValue) {
try {
BaseSysPermsAuditLogEntity auditLog = new BaseSysPermsAuditLogEntity();
auditLog.setOperatorUserId(CoolSecurityUtil.getCurrentUserId());
auditLog.setOperatorUsername(CoolSecurityUtil.getAdminUsername());
auditLog.setChangeType(changeType);
auditLog.setTargetId(targetId);
auditLog.setBeforeValue(beforeValue);
auditLog.setAfterValue(afterValue);
baseSysPermsAuditLogMapper.insertSelective(auditLog);
} catch (Exception e) {
log.warn("记录权限审计日志失败: {}", e.getMessage());
}
}
}
@@ -9,12 +9,16 @@ import com.cool.core.util.CoolSecurityUtil;
import com.cool.modules.base.entity.sys.BaseSysRoleDepartmentEntity;
import com.cool.modules.base.entity.sys.BaseSysRoleEntity;
import com.cool.modules.base.entity.sys.BaseSysRoleMenuEntity;
import com.cool.modules.base.entity.sys.BaseSysUserRoleEntity;
import com.cool.modules.base.mapper.sys.BaseSysRoleDepartmentMapper;
import com.cool.modules.base.mapper.sys.BaseSysRoleMapper;
import com.cool.modules.base.mapper.sys.BaseSysRoleMenuMapper;
import com.cool.modules.base.mapper.sys.BaseSysUserRoleMapper;
import com.cool.modules.base.service.sys.BaseSysPermsService;
import com.cool.modules.base.service.sys.BaseSysRoleService;
import com.mybatisflex.core.query.QueryWrapper;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -35,6 +39,8 @@ public class BaseSysRoleServiceImpl extends BaseServiceImpl<BaseSysRoleMapper, B
final private BaseSysPermsService baseSysPermsService;
final private BaseSysUserRoleMapper baseSysUserRoleMapper;
@Override
public Object add(JSONObject requestParams, BaseSysRoleEntity entity) {
BaseSysRoleEntity checkLabel = getOne(QueryWrapper.create().eq(BaseSysRoleEntity::getLabel, entity.getLabel()));
@@ -46,7 +52,7 @@ public class BaseSysRoleServiceImpl extends BaseServiceImpl<BaseSysRoleMapper, B
}
@Override
public Object info(Long id) {
public BaseSysRoleEntity info(Long id) {
BaseSysRoleEntity roleEntity = getById(id);
Long[] menuIdList = new Long[0];
Long[] departmentIdList = new Long[0];
@@ -60,8 +66,14 @@ public class BaseSysRoleServiceImpl extends BaseServiceImpl<BaseSysRoleMapper, B
departmentIdList = departmentEntities.stream().map(BaseSysRoleDepartmentEntity::getDepartmentId)
.toArray(Long[]::new);
roleEntity.setMenuIdList(Arrays.asList( menuIdList ));
roleEntity.setDepartmentIdList(Arrays.asList(departmentIdList ));
}
return Dict.parse(roleEntity).set("menuIdList", menuIdList).set("departmentIdList", departmentIdList);
return roleEntity;
}
@Override
@@ -69,11 +81,20 @@ public class BaseSysRoleServiceImpl extends BaseServiceImpl<BaseSysRoleMapper, B
if (type == ModifyEnum.DELETE) {
Long[] ids = requestParams.get("ids", Long[].class);
for (Long id : ids) {
baseSysRoleMenuMapper.deleteByQuery(
QueryWrapper.create().eq(BaseSysRoleMenuEntity::getRoleId, id));
baseSysRoleDepartmentMapper.deleteByQuery(
QueryWrapper.create().eq(BaseSysRoleDepartmentEntity::getRoleId, id));
baseSysUserRoleMapper.deleteByQuery(
QueryWrapper.create().eq(BaseSysUserRoleEntity::getRoleId, id));
baseSysPermsService.refreshPermsByRoleId(id);
}
} else {
baseSysPermsService.updatePerms(baseSysRoleEntity.getId(), requestParams.get("menuIdList", Long[].class),
requestParams.get("departmentIdList", Long[].class));
if (baseSysRoleEntity.getStatus() != null && baseSysRoleEntity.getStatus() == 0) {
baseSysPermsService.refreshPermsByRoleId(baseSysRoleEntity.getId());
}
}
}
@@ -85,6 +106,6 @@ public class BaseSysRoleServiceImpl extends BaseServiceImpl<BaseSysRoleMapper, B
w.in(BaseSysRoleEntity::getId,
(Object) object.get("roleIds", Long[].class));
});
}, !CoolSecurityUtil.getAdminUsername().equals("admin")));
}, !CoolSecurityUtil.isSuperAdmin()));
}
}
@@ -6,10 +6,12 @@ import static com.cool.modules.base.entity.sys.table.BaseSysUserEntityTableDef.B
import static com.cool.modules.base.entity.sys.table.BaseSysUserRoleEntityTableDef.BASE_SYS_USER_ROLE_ENTITY;
import static com.mybatisflex.core.query.QueryMethods.groupConcat;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.json.JSONObject;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.base.ModifyEnum;
@@ -18,23 +20,30 @@ import com.cool.core.exception.CoolPreconditions;
import com.cool.core.util.CoolSecurityUtil;
import com.cool.core.util.DatabaseDialectUtils;
import com.cool.modules.base.entity.sys.BaseSysDepartmentEntity;
import com.cool.modules.base.entity.sys.BaseSysRoleEntity;
import com.cool.modules.base.entity.sys.BaseSysUserEntity;
import com.cool.modules.base.entity.sys.BaseSysUserRoleEntity;
import com.cool.modules.base.mapper.sys.BaseSysDepartmentMapper;
import com.cool.modules.base.mapper.sys.BaseSysRoleMapper;
import com.cool.modules.base.mapper.sys.BaseSysUserMapper;
import com.cool.modules.base.mapper.sys.BaseSysUserRoleMapper;
import com.cool.modules.base.service.sys.BaseSysPermsService;
import com.cool.modules.base.service.sys.BaseSysUserService;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.update.UpdateChain;
import lombok.RequiredArgsConstructor;
import org.dromara.autotable.core.constants.DatabaseDialect;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
/**
* 系统用户
*/
@Service
@RequiredArgsConstructor
public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, BaseSysUserEntity>
implements BaseSysUserService {
@@ -44,17 +53,40 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
final private BaseSysDepartmentMapper baseSysDepartmentMapper;
final private BaseSysUserRoleMapper baseSysUserRoleMapper;
final private BaseSysRoleMapper baseSysRoleMapper;
final private PasswordEncoder passwordEncoder;
public BaseSysUserServiceImpl(CoolCache coolCache, BaseSysPermsService baseSysPermsService,
BaseSysDepartmentMapper baseSysDepartmentMapper, BaseSysUserRoleMapper baseSysUserRoleMapper,
BaseSysRoleMapper baseSysRoleMapper, @Lazy PasswordEncoder passwordEncoder) {
this.coolCache = coolCache;
this.baseSysPermsService = baseSysPermsService;
this.baseSysDepartmentMapper = baseSysDepartmentMapper;
this.baseSysUserRoleMapper = baseSysUserRoleMapper;
this.baseSysRoleMapper = baseSysRoleMapper;
this.passwordEncoder = passwordEncoder;
}
@Override
public Object page(JSONObject requestParams, Page<BaseSysUserEntity> page, QueryWrapper qw) {
String keyWord = requestParams.getStr("keyWord");
Integer status = requestParams.getInt("status");
Long[] departmentIds = requestParams.get("departmentIds", Long[].class);
JSONObject tokenInfo = CoolSecurityUtil.getAdminUserInfo(requestParams);
// 用户的部门权限
Long[] permsDepartmentArr = coolCache.get("admin:department:" + tokenInfo.get("userId"),
Long[].class);
String databaseDialect = DatabaseDialectUtils.getDatabaseDialect();
if (databaseDialect.equals(DatabaseDialect.PostgreSQL)) {
CoolPreconditions.checkEmpty(departmentIds);
Collection<Long> intersectionDep = Convert.toList(Long.class, departmentIds);
if (!CoolSecurityUtil.isSuperAdmin()) {
// 用户的部门权限
Long[] permsDepartmentArr = baseSysPermsService.getDepartmentIdsByUserId(CoolSecurityUtil.getCurrentUserId());
CoolPreconditions.returnNoData(permsDepartmentArr);
// 取交集
intersectionDep = CollUtil.intersection(Convert.toList(Long.class, departmentIds), Convert.toList(Long.class, permsDepartmentArr));
CoolPreconditions.returnNoData(intersectionDep);
}
if (DatabaseDialectUtils.isPostgresql()) {
// 兼容postgresql
qw.select("base_sys_user.id","base_sys_user.create_time","base_sys_user.department_id",
"base_sys_user.email","base_sys_user.head_img","base_sys_user.name","base_sys_user.nick_name",
@@ -77,11 +109,12 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
.leftJoin(BASE_SYS_DEPARTMENT_ENTITY)
.on(BASE_SYS_USER_ENTITY.DEPARTMENT_ID.eq(BASE_SYS_DEPARTMENT_ENTITY.ID));
// 不显示admin用户
qw.and(BASE_SYS_USER_ENTITY.USERNAME.ne("admin"));
// 筛选部门
qw.and(BASE_SYS_USER_ENTITY.DEPARTMENT_ID.in(departmentIds,
ArrayUtil.isNotEmpty(departmentIds)));
// 不显示超管用户
qw.and(BASE_SYS_USER_ENTITY.ID.notIn(
baseSysUserRoleMapper.selectListByQuery(
QueryWrapper.create().eq(BaseSysUserRoleEntity::getRoleId, 1L)
).stream().map(BaseSysUserRoleEntity::getUserId).toArray()
));
// 筛选状态
qw.and(BASE_SYS_USER_ENTITY.STATUS.eq(status, status != null));
// 搜索关键字
@@ -90,11 +123,8 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
.or(BASE_SYS_USER_ENTITY.USERNAME.like(keyWord)));
}
// 过滤部门权限
qw.and(BASE_SYS_USER_ENTITY.DEPARTMENT_ID.in(
permsDepartmentArr == null || permsDepartmentArr.length == 0 ? new Long[]{null}
: permsDepartmentArr,
!CoolSecurityUtil.getAdminUsername().equals("admin")));
if (databaseDialect.equals(DatabaseDialect.PostgreSQL)) {
qw.and(BASE_SYS_USER_ENTITY.DEPARTMENT_ID.in(intersectionDep, ArrayUtil.isNotEmpty(intersectionDep)));
if (DatabaseDialectUtils.isPostgresql()) {
// 兼容postgresql
qw.groupBy("base_sys_user.id","base_sys_user.create_time","base_sys_user.department_id",
"base_sys_user.email","base_sys_user.head_img","base_sys_user.name","base_sys_user.nick_name",
@@ -113,9 +143,9 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
CoolPreconditions.checkEmpty(userEntity, "用户不存在");
userEntity.setNickName(body.getStr("nickName"));
userEntity.setHeadImg(body.getStr("headImg"));
// 修改密码
if (StrUtil.isNotEmpty(body.getStr("password"))) {
userEntity.setPassword(MD5.create().digestHex(body.getStr("password")));
if (StrUtil.isNotEmpty(body.getStr("password")) && StrUtil.isNotEmpty(body.getStr("newPassword"))) {
CoolPreconditions.check(!passwordEncoder.matches(body.getStr("password"), userEntity.getPassword()), "原密码错误");
userEntity.setPassword(passwordEncoder.encode(body.getStr("newPassword")));
userEntity.setPasswordV(userEntity.getPasswordV() + 1);
coolCache.set("admin:passwordVersion:" + userId, userEntity.getPasswordV());
}
@@ -124,6 +154,7 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
@Override
public void move(Long departmentId, Long[] userIds) {
CoolPreconditions.check(!baseSysPermsService.hasDepartmentPermission(departmentId), "无权将用户移动到该部门");
UpdateChain.of(BaseSysUserEntity.class)
.set(BaseSysUserEntity::getDepartmentId, departmentId)
.in(BaseSysUserEntity::getId, (Object) userIds).update();
@@ -134,7 +165,7 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
BaseSysUserEntity check = getOne(
QueryWrapper.create().eq(BaseSysUserEntity::getUsername, entity.getUsername()));
CoolPreconditions.check(check != null, "用户名已存在");
entity.setPassword(MD5.create().digestHex(entity.getPassword()));
entity.setPassword(passwordEncoder.encode(entity.getPassword()));
super.add(requestParams, entity);
return entity.getId();
}
@@ -142,18 +173,18 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
@Override
public boolean update(JSONObject requestParams, BaseSysUserEntity entity) {
CoolPreconditions.check(
StrUtil.isNotEmpty(entity.getUsername()) && entity.getUsername().equals("admin"),
StrUtil.isNotEmpty(entity.getUsername()) && CoolSecurityUtil.isSuperAdmin(entity.getUsername()),
"非法操作");
BaseSysUserEntity userEntity = getById(entity.getId());
if (StrUtil.isNotEmpty(entity.getPassword())) {
entity.setPasswordV(entity.getPasswordV() + 1);
entity.setPassword(MD5.create().digestHex(entity.getPassword()));
String requestPassword = requestParams.getStr("password");
if (StrUtil.isNotEmpty(requestPassword)) {
entity.setPasswordV(userEntity.getPasswordV() + 1);
entity.setPassword(passwordEncoder.encode(requestPassword));
coolCache.set("admin:passwordVersion:" + entity.getId(), entity.getPasswordV());
} else {
entity.setPassword(userEntity.getPassword());
entity.setPasswordV(userEntity.getPasswordV());
}
// 被禁用
if (entity.getStatus() == 0) {
CoolSecurityUtil.adminLogout(entity);
}
@@ -163,21 +194,56 @@ public class BaseSysUserServiceImpl extends BaseServiceImpl<BaseSysUserMapper, B
@Override
public void modifyAfter(JSONObject requestParams, BaseSysUserEntity baseSysUserEntity,
ModifyEnum type) {
if (type != ModifyEnum.DELETE && requestParams.get("roleIdList", Long[].class) != null) {
// 刷新权限
baseSysPermsService.updateUserRole(baseSysUserEntity.getId(),
requestParams.get("roleIdList", Long[].class));
if (type == ModifyEnum.DELETE) {
Long[] ids = requestParams.get("ids", Long[].class);
if (ids != null) {
for (Long id : ids) {
baseSysUserRoleMapper.deleteByQuery(
QueryWrapper.create().eq(BaseSysUserRoleEntity::getUserId, id));
CoolSecurityUtil.adminLogout(id, null);
}
}
} else if (requestParams.get("roleIdList", Long[].class) != null) {
Long[] roleIdList = requestParams.get("roleIdList", Long[].class);
validateRoleAssignment(roleIdList);
baseSysPermsService.updateUserRole(baseSysUserEntity.getId(), roleIdList);
}
}
private void validateRoleAssignment(Long[] roleIdList) {
if (ArrayUtil.isEmpty(roleIdList)) {
return;
}
if (CoolSecurityUtil.isSuperAdmin()) {
return;
}
List<Long> requestedRoles = CollUtil.toList(roleIdList);
CoolPreconditions.check(requestedRoles.contains(1L), "无权分配超级管理员角色");
Long[] currentRoleIds = baseSysPermsService.getRoles(CoolSecurityUtil.getCurrentUserId());
if (ObjectUtil.isEmpty(currentRoleIds)) {
CoolPreconditions.check(true, "无权分配任何角色");
}
List<Long> allowedRoles = CollUtil.toList(currentRoleIds);
for (Long roleId : requestedRoles) {
BaseSysRoleEntity role = baseSysRoleMapper.selectOneById(roleId);
CoolPreconditions.checkEmpty(role, "角色不存在: {}", roleId);
CoolPreconditions.check(!allowedRoles.contains(roleId), "无权分配角色: {}", role.getName());
}
}
@Override
public Object info(Long id) {
public BaseSysUserEntity info(Long id) {
BaseSysUserEntity userEntity = getById(id);
CoolPreconditions.checkEmpty(userEntity, "用户不存在");
if (!CoolSecurityUtil.isSuperAdmin()) {
CoolPreconditions.check(!baseSysPermsService.hasDepartmentPermission(userEntity.getDepartmentId()), "无权查看该用户信息");
}
Long[] roleIdList = baseSysPermsService.getRoles(id);
BaseSysDepartmentEntity departmentEntity = baseSysDepartmentMapper.selectOneById(
userEntity.getDepartmentId());
userEntity.setPassword(null);
return Dict.parse(userEntity).set("roleIdList", roleIdList).set("departmentName",
departmentEntity != null ? departmentEntity.getName() : null);
userEntity.setRoleIdList(List.of(roleIdList));
userEntity.setDepartmentName(departmentEntity != null ? departmentEntity.getName() : userEntity.getDepartmentName());
return userEntity;
}
}
@@ -5,7 +5,9 @@ import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.annotation.TokenIgnore;
import com.cool.core.base.BaseController;
import com.cool.core.request.CrudOption;
import com.cool.core.request.R;
import com.cool.core.util.I18nUtil;
import com.cool.modules.dict.entity.DictInfoEntity;
import com.cool.modules.dict.entity.table.DictInfoEntityTableDef;
import com.cool.modules.dict.service.DictInfoService;
@@ -26,8 +28,18 @@ public class AdminDictInfoController extends BaseController<DictInfoService, Dic
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
setListOption(createOp().fieldEq(DictInfoEntityTableDef.DICT_INFO_ENTITY.TYPE_ID)
.keyWordLikeFields(DictInfoEntityTableDef.DICT_INFO_ENTITY.NAME)
.queryWrapper(QueryWrapper.create().orderBy(DictInfoEntityTableDef.DICT_INFO_ENTITY.CREATE_TIME, false)));
.keyWordLikeFields(DictInfoEntityTableDef.DICT_INFO_ENTITY.NAME)
.queryWrapper(QueryWrapper.create().orderBy(DictInfoEntityTableDef.DICT_INFO_ENTITY.CREATE_TIME, false))
.transformValue(o -> {
DictInfoEntity entity = (DictInfoEntity) o;
entity.setName(I18nUtil.getI18nDictInfo(entity.getName()));
}));
CrudOption<DictInfoEntity> transform = createOp().transformValue(o -> {
DictInfoEntity entity = (DictInfoEntity) o;
entity.setName(I18nUtil.getI18nDictInfo(entity.getName()));
});
setPageOption(transform);
setInfoOption(transform);
}
@Operation(summary = "获得字典数据", description = "获得字典数据信息")
@@ -1,15 +1,17 @@
package com.cool.modules.dict.controller.admin;
import static com.cool.modules.dict.entity.table.DictTypeEntityTableDef.DICT_TYPE_ENTITY;
import cn.hutool.json.JSONObject;
import com.cool.core.annotation.CoolRestController;
import com.cool.core.base.BaseController;
import com.cool.core.request.CrudOption;
import com.cool.core.util.I18nUtil;
import com.cool.modules.dict.entity.DictTypeEntity;
import com.cool.modules.dict.service.DictTypeService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import static com.cool.modules.dict.entity.table.DictTypeEntityTableDef.DICT_TYPE_ENTITY;
/**
* 字典类型
*/
@@ -20,6 +22,15 @@ public class AdminDictTypeController extends BaseController<DictTypeService, Dic
@Override
protected void init(HttpServletRequest request, JSONObject requestParams) {
setPageOption(
createOp().select(DICT_TYPE_ENTITY.ID, DICT_TYPE_ENTITY.KEY, DICT_TYPE_ENTITY.NAME));
createOp().select(DICT_TYPE_ENTITY.ID, DICT_TYPE_ENTITY.KEY, DICT_TYPE_ENTITY.NAME).transformValue(o -> {
DictTypeEntity entity = (DictTypeEntity) o;
entity.setName(I18nUtil.getI18nDictType(entity.getName()));
}));
CrudOption<DictTypeEntity> transform = createOp().transformValue(o -> {
DictTypeEntity entity = (DictTypeEntity) o;
entity.setName(I18nUtil.getI18nDictType(entity.getName()));
});
setPageOption(transform);
setInfoOption(transform);
}
}
@@ -1,7 +1,7 @@
package com.cool.modules.dict.entity;
import com.cool.core.base.BaseEntity;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import com.mybatisflex.annotation.Table;
import lombok.Getter;
import lombok.Setter;
@@ -2,7 +2,7 @@ package com.cool.modules.dict.entity;
import com.cool.core.base.BaseEntity;
import com.mybatisflex.annotation.Table;
import com.tangzc.mybatisflex.autotable.annotation.ColumnDefine;
import com.cool.core.annotation.ColumnDefine;
import com.tangzc.mybatisflex.autotable.annotation.UniIndex;
import lombok.Getter;
import lombok.Setter;
@@ -8,7 +8,11 @@ import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.base.ModifyEnum;
import com.cool.core.i18n.I18nGenerator;
import com.cool.modules.dict.entity.DictInfoEntity;
import com.cool.modules.dict.entity.DictTypeEntity;
import com.cool.modules.dict.mapper.DictInfoMapper;
@@ -121,4 +125,10 @@ public class DictInfoServiceImpl extends BaseServiceImpl<DictInfoMapper, DictInf
}
}
}
@Override
public void modifyAfter(JSONObject requestParams, DictInfoEntity entity, ModifyEnum type) {
if (ModifyEnum.ADD.equals(type) || ModifyEnum.UPDATE.equals(type)) {
SpringUtil.getBean(I18nGenerator.class).asyncGenBaseDictInfo();
}
}
}
@@ -3,7 +3,11 @@ package com.cool.modules.dict.service.impl;
import static com.cool.modules.dict.entity.table.DictInfoEntityTableDef.DICT_INFO_ENTITY;
import static com.cool.modules.dict.entity.table.DictTypeEntityTableDef.DICT_TYPE_ENTITY;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONObject;
import com.cool.core.base.BaseServiceImpl;
import com.cool.core.base.ModifyEnum;
import com.cool.core.i18n.I18nGenerator;
import com.cool.modules.dict.entity.DictTypeEntity;
import com.cool.modules.dict.mapper.DictInfoMapper;
import com.cool.modules.dict.mapper.DictTypeMapper;
@@ -36,4 +40,11 @@ public class DictTypeServiceImpl extends BaseServiceImpl<DictTypeMapper, DictTyp
return dictInfoMapper.deleteByQuery(
QueryWrapper.create().and(DICT_INFO_ENTITY.TYPE_ID.in((Object) ids))) > 0;
}
@Override
public void modifyBefore(JSONObject requestParams, DictTypeEntity t, ModifyEnum type) {
if (ModifyEnum.ADD.equals(type) || ModifyEnum.UPDATE.equals(type)) {
SpringUtil.getBean(I18nGenerator.class).asyncGenBaseDictType();
}
}
}

Some files were not shown because too many files have changed in this diff Show More